Allow configured providers to provide additional functions. (#1491)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-04-18 09:11:38 -04:00 committed by GitHub
parent 0ea88633d0
commit a69d19d9f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 720 additions and 276 deletions

2
go.mod
View File

@ -267,3 +267,5 @@ require (
)
go 1.21
replace github.com/hashicorp/hcl/v2 v2.20.1 => github.com/opentofu/hcl/v2 v2.0.0-20240416130056-03228b26f391

4
go.sum
View File

@ -716,8 +716,6 @@ 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.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=
@ -927,6 +925,8 @@ github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/openbao/openbao/api v0.0.0-20240326035453-c075f0ef2c7e h1:LIQFfqW6BA5E2ycx8NNDgyKh0exFubHePM5pF3knogo=
github.com/openbao/openbao/api v0.0.0-20240326035453-c075f0ef2c7e/go.mod h1:NUvBdXCNlmAGQ9TbYV7vS1Y9awHAjrq3QLiBWV+4Glk=
github.com/opentofu/hcl/v2 v2.0.0-20240416130056-03228b26f391 h1:Z2YGMhYBvmXBZlQdnlembuV4sp0lPJphIfgM9fVSjpU=
github.com/opentofu/hcl/v2 v2.0.0-20240416130056-03228b26f391/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4=
github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633 h1:81TBkM/XGIFlVvyabp0CJl00UHeVUiQjz0fddLMi848=
github.com/opentofu/registry-address v0.0.0-20230920144404-f1e51167f633/go.mod h1:HzQhpVo/NJnGmN+7FPECCVCA5ijU7AUcvf39enBKYOc=
github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU=

View File

@ -340,7 +340,6 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
SourceRange: tfdiags.SourceRangeFromHCL(rng),
Remaining: remain,
}, diags
case "template", "lazy", "arg":
// These names are all pre-emptively reserved in the hope of landing
// some version of "template values" or "lazy expressions" feature
@ -354,6 +353,22 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) {
return nil, diags
default:
function := ParseFunction(root)
if function.IsNamespace(FunctionNamespaceProvider) {
pf, err := function.AsProviderFunction()
if err != nil {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unable to parse provider function",
Detail: err.Error(),
Subject: rootRange.Ptr(),
})
}
return &Reference{
Subject: pf,
SourceRange: tfdiags.SourceRangeFromHCL(rootRange),
}, diags
}
return parseResourceRef(ManagedResourceMode, rootRange, traversal)
}
}

View File

@ -0,0 +1,78 @@
package addrs
import (
"fmt"
"strings"
)
// ProviderFunction is the address of a provider defined function.
type ProviderFunction struct {
referenceable
ProviderName string
ProviderAlias string
Function string
}
func (v ProviderFunction) String() string {
if v.ProviderAlias != "" {
return fmt.Sprintf("provider::%s::%s::%s", v.ProviderName, v.ProviderAlias, v.Function)
}
return fmt.Sprintf("provider::%s::%s", v.ProviderName, v.Function)
}
func (v ProviderFunction) UniqueKey() UniqueKey {
return v // A ProviderFunction is its own UniqueKey
}
func (v ProviderFunction) uniqueKeySigil() {}
type Function struct {
Namespaces []string
Name string
}
const (
FunctionNamespaceProvider = "provider"
FunctionNamespaceCore = "core"
)
var FunctionNamespaces = []string{
FunctionNamespaceProvider,
FunctionNamespaceCore,
}
func ParseFunction(input string) Function {
parts := strings.Split(input, "::")
return Function{
Name: parts[len(parts)-1],
Namespaces: parts[:len(parts)-1],
}
}
func (f Function) String() string {
return strings.Join(append(f.Namespaces, f.Name), "::")
}
func (f Function) IsNamespace(namespace string) bool {
return len(f.Namespaces) > 0 && f.Namespaces[0] == namespace
}
func (f Function) AsProviderFunction() (pf ProviderFunction, err error) {
if !f.IsNamespace(FunctionNamespaceProvider) {
// Should always be checked ahead of time!
panic("BUG: non-provider function " + f.String())
}
if len(f.Namespaces) == 2 {
// provider::<name>::<function>
pf.ProviderName = f.Namespaces[1]
} else if len(f.Namespaces) == 3 {
// provider::<name>::<alias>::<function>
pf.ProviderName = f.Namespaces[1]
pf.ProviderAlias = f.Namespaces[2]
} else {
return pf, fmt.Errorf("invalid provider function %q: expected provider::<name>::<function> or provider::<name>::<alias>::<function>", f)
}
pf.Function = f.Name
return pf, nil
}

View File

@ -160,6 +160,10 @@ func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRe
return validateDataStoreResourceConfig(req)
}
func (p *Provider) GetFunctions() providers.GetFunctionsResponse {
panic("unimplemented - terraform provider has no functions")
}
func (p *Provider) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
panic("unimplemented - terraform provider has no functions")
}

View File

@ -249,6 +249,16 @@ func (e *fixupBlocksExpr) Variables() []hcl.Traversal {
return ret
}
func (e *fixupBlocksExpr) Functions() []hcl.Traversal {
var ret []hcl.Traversal
schema := SchemaForCtyElementType(e.ety)
spec := schema.DecoderSpec()
for _, block := range e.blocks {
ret = append(ret, hcldec.Functions(block.Body, spec)...)
}
return ret
}
func (e *fixupBlocksExpr) Range() hcl.Range {
// This is not really an appropriate range for the expression but it's
// the best we can do from here.

View File

@ -0,0 +1,45 @@
package blocktoattr
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/dynblock"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/opentofu/opentofu/internal/configs/configschema"
)
// ExpandedFunctions finds all of the global functions referenced in the
// given body with the given schema while taking into account the possibilities
// both of "dynamic" blocks being expanded and the possibility of certain
// attributes being written instead as nested blocks as allowed by the
// FixUpBlockAttrs function.
//
// This function exists to allow functions to be analyzed prior to dynamic
// block expansion while also dealing with the fact that dynamic block expansion
// might in turn produce nested blocks that are subject to FixUpBlockAttrs.
//
// This is intended as a drop-in replacement for dynblock.FunctionsHCLDec,
// which is itself a drop-in replacement for hcldec.Functions.
func ExpandedFunctions(body hcl.Body, schema *configschema.Block) []hcl.Traversal {
rootNode := dynblock.WalkFunctions(body)
return walkFunctions(rootNode, body, schema)
}
func walkFunctions(node dynblock.WalkFunctionsNode, body hcl.Body, schema *configschema.Block) []hcl.Traversal {
givenRawSchema := hcldec.ImpliedSchema(schema.DecoderSpec())
ambiguousNames := ambiguousNames(schema)
effectiveRawSchema := effectiveSchema(givenRawSchema, body, ambiguousNames, false)
vars, children := node.Visit(effectiveRawSchema)
for _, child := range children {
if blockS, exists := schema.BlockTypes[child.BlockTypeName]; exists {
vars = append(vars, walkFunctions(child.Node, child.Body(), &blockS.Block)...)
} else if attrS, exists := schema.Attributes[child.BlockTypeName]; exists && attrS.Type.IsCollectionType() && attrS.Type.ElementType().IsObjectType() {
// ☝Check for collection type before element type, because if this is a mis-placed reference,
// a panic here will prevent other useful diags from being elevated to show the user what to fix
synthSchema := SchemaForCtyElementType(attrS.Type.ElementType())
vars = append(vars, walkFunctions(child.Node, child.Body(), synthSchema)...)
}
}
return vars
}

View File

@ -7,7 +7,6 @@ package lang
import (
"fmt"
"regexp"
"strings"
"github.com/hashicorp/hcl/v2"
@ -199,9 +198,6 @@ 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))
@ -213,7 +209,7 @@ func (s *Scope) enhanceFunctionDiags(diags hcl.Diagnostics) hcl.Diagnostics {
// prefix::stuff::
fullNamespace := funcExtra.CalledFunctionNamespace()
if !strings.Contains(fullNamespace, "::") {
if len(fullNamespace) == 0 {
// Not a namespaced function, no enhancements nessesary
continue
}
@ -224,32 +220,23 @@ func (s *Scope) enhanceFunctionDiags(diags hcl.Diagnostics) hcl.Diagnostics {
// Update enhanced with additional details
if fullNamespace == CoreNamespace {
fn := addrs.ParseFunction(fullNamespace + funcName)
if fn.IsNamespace(addrs.FunctionNamespaceCore) {
// 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)
enhanced.Detail = fmt.Sprintf("There is no builtin (%s::) function named %q.", addrs.FunctionNamespaceCore, funcName)
} else if fn.IsNamespace(addrs.FunctionNamespaceProvider) {
if _, err := fn.AsProviderFunction(); err != nil {
// complete mismatch or invalid prefix
enhanced.Summary = "Invalid function format"
enhanced.Detail = err.Error()
}
} 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)
enhanced.Summary = "Unknown function namespace"
enhanced.Detail = fmt.Sprintf("Function %q does not exist within a valid namespace (%s)", fn, strings.Join(addrs.FunctionNamespaces, ","))
}
// Function / Provider not found handled by eval_context_builtin.go
}
}
return out
@ -478,7 +465,16 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
val, valDiags := normalizeRefValue(s.Data.GetCheckBlock(subj, rng))
diags = diags.Append(valDiags)
outputValues[subj.Name] = val
case addrs.ProviderFunction:
// Inject function directly into context
if _, ok := ctx.Functions[subj.String()]; !ok {
fn, fnDiags := s.ProviderFunctions(subj, rng)
diags = diags.Append(fnDiags)
if !fnDiags.HasErrors() {
ctx.Functions[subj.String()] = *fn
}
}
default:
// Should never happen
panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))

View File

@ -874,26 +874,14 @@ func Test_enhanceFunctionDiags(t *testing.T) {
{
"Invalid prefix",
"attr = magic::missing_function(54)",
"Unknown function namespace",
"Function \"magic::missing_function\" does not exist within a valid namespace (provider,core)",
},
{
"Too many namespaces",
"attr = provider::foo::bar::extra::extra2::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\"",
"invalid provider function \"provider::foo::bar::extra::extra2::missing_function\": expected provider::<name>::<function> or provider::<name>::<alias>::<function>",
},
}
@ -919,15 +907,7 @@ func Test_enhanceFunctionDiags(t *testing.T) {
body := file.Body
scope := &Scope{
ProviderNames: map[string]addrs.Provider{
"known": addrs.Provider{
Type: "type",
Namespace: "namespace",
Hostname: "hostname",
},
},
}
scope := &Scope{}
ctx, ctxDiags := scope.EvalContext(nil)
if ctxDiags.HasErrors() {

View File

@ -14,6 +14,7 @@ import (
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/experiments"
"github.com/opentofu/opentofu/internal/lang/funcs"
)
@ -24,7 +25,8 @@ var impureFunctions = []string{
"uuid",
}
const CoreNamespace = "core::"
// This should probably be replaced with addrs.Function everywhere
const CoreNamespace = addrs.FunctionNamespaceCore + "::"
// Functions returns the set of functions that should be used to when evaluating
// expressions in the receiving scope.
@ -204,10 +206,6 @@ func (s *Scope) Functions() map[string]function.Function {
for _, name := range coreNames {
s.funcs[CoreNamespace+name] = s.funcs[name]
}
for name, f := range s.ProviderFunctions {
s.funcs[name] = f
}
}
s.funcsLock.Unlock()

View File

@ -72,7 +72,9 @@ func ReferencesInBlock(parseRef ParseRef, body hcl.Body, schema *configschema.Bl
// in a better position to test this due to having mock providers etc
// available.
traversals := blocktoattr.ExpandedVariables(body, schema)
return References(parseRef, traversals)
funcs := filterProviderFunctions(blocktoattr.ExpandedFunctions(body, schema))
return References(parseRef, append(traversals, funcs...))
}
// ReferencesInExpr is a helper wrapper around References that first searches
@ -83,5 +85,24 @@ func ReferencesInExpr(parseRef ParseRef, expr hcl.Expression) ([]*addrs.Referenc
return nil, nil
}
traversals := expr.Variables()
if fexpr, ok := expr.(hcl.ExpressionWithFunctions); ok {
funcs := filterProviderFunctions(fexpr.Functions())
traversals = append(traversals, funcs...)
}
return References(parseRef, traversals)
}
func filterProviderFunctions(funcs []hcl.Traversal) []hcl.Traversal {
pfuncs := make([]hcl.Traversal, 0, len(funcs))
for _, fn := range funcs {
if len(fn) == 0 {
continue
}
if root, ok := fn[0].(hcl.TraverseRoot); ok {
if addrs.ParseFunction(root.Name).IsNamespace(addrs.FunctionNamespaceProvider) {
pfuncs = append(pfuncs, fn)
}
}
}
return pfuncs
}

View File

@ -72,10 +72,11 @@ type Scope struct {
// 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
ProviderFunctions ProviderFunction
}
type ProviderFunction func(addrs.ProviderFunction, tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics)
// SetActiveExperiments allows a caller to declare that a set of experiments
// is active for the module that the receiving Scope belongs to, which might
// then cause the scope to activate some additional experimental behaviors.

View File

@ -362,6 +362,10 @@ func (p *MockProvider) ReadDataSource(r providers.ReadDataSourceRequest) provide
return p.ReadDataSourceResponse
}
func (p *MockProvider) GetFunctions() providers.GetFunctionsResponse {
panic("Not Implemented")
}
func (p *MockProvider) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
panic("Not Implemented")
}

View File

@ -693,6 +693,26 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p
return resp
}
func (p *GRPCProvider) GetFunctions() (resp providers.GetFunctionsResponse) {
logger.Trace("GRPCProvider: GetFunctions")
protoReq := &proto.GetFunctions_Request{}
protoResp, err := p.client.GetFunctions(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
resp.Functions = make(map[string]providers.FunctionSpec)
for name, fn := range protoResp.Functions {
resp.Functions[name] = convert.ProtoToFunctionSpec(fn)
}
return resp
}
func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
logger.Trace("GRPCProvider: CallFunction")
@ -705,9 +725,18 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
spec, ok := schema.Functions[r.Name]
if !ok {
// This should be unreachable
resp.Error = fmt.Errorf("invalid CallFunctionRequest: function %s not defined in provider schema", r.Name)
return resp
funcs := p.GetFunctions()
if funcs.Diagnostics.HasErrors() {
// This should be unreachable
resp.Error = funcs.Diagnostics.Err()
return resp
}
spec, ok = funcs.Functions[r.Name]
if !ok {
// This should be unreachable
resp.Error = fmt.Errorf("invalid CallFunctionRequest: function %s not defined in provider schema", r.Name)
return resp
}
}
protoReq := &proto.CallFunction_Request{

View File

@ -682,6 +682,26 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p
return resp
}
func (p *GRPCProvider) GetFunctions() (resp providers.GetFunctionsResponse) {
logger.Trace("GRPCProvider6: GetFunctions")
protoReq := &proto6.GetFunctions_Request{}
protoResp, err := p.client.GetFunctions(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
resp.Functions = make(map[string]providers.FunctionSpec)
for name, fn := range protoResp.Functions {
resp.Functions[name] = convert.ProtoToFunctionSpec(fn)
}
return resp
}
func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
logger.Trace("GRPCProvider6: CallFunction")
@ -694,9 +714,18 @@ func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp provi
spec, ok := schema.Functions[r.Name]
if !ok {
// This should be unreachable
resp.Error = fmt.Errorf("invalid CallFunctionRequest: function %s not defined in provider schema", r.Name)
return resp
funcs := p.GetFunctions()
if funcs.Diagnostics.HasErrors() {
// This should be unreachable
resp.Error = funcs.Diagnostics.Err()
return resp
}
spec, ok = funcs.Functions[r.Name]
if !ok {
// This should be unreachable
resp.Error = fmt.Errorf("invalid CallFunctionRequest: function %s not defined in provider schema", r.Name)
return resp
}
}
protoReq := &proto6.CallFunction_Request{

View File

@ -147,6 +147,10 @@ func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp provid
return resp
}
func (s simple) GetFunctions() providers.GetFunctionsResponse {
panic("Not Implemented")
}
func (s simple) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
panic("Not Implemented")
}

View File

@ -138,6 +138,10 @@ func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp provid
return resp
}
func (s simple) GetFunctions() providers.GetFunctionsResponse {
panic("Not Implemented")
}
func (s simple) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
panic("Not Implemented")
}

View File

@ -77,9 +77,9 @@ type Interface interface {
// ReadDataSource returns the data source's current state.
ReadDataSource(ReadDataSourceRequest) ReadDataSourceResponse
// GetFunctions not yet implemented or used at this stage as it is not required.
// tofu queries a full set of provider schemas early on in the process which contain
// the required information.
// GetFunctions returns a full list of functions defined in this provider. It should be a super
// set of the functions returned in GetProviderSchema()
GetFunctions() GetFunctionsResponse
// CallFunction requests that the given function is called and response returned.
CallFunction(CallFunctionRequest) CallFunctionResponse
@ -475,6 +475,12 @@ type ReadDataSourceResponse struct {
Diagnostics tfdiags.Diagnostics
}
type GetFunctionsResponse struct {
Functions map[string]FunctionSpec
Diagnostics tfdiags.Diagnostics
}
type CallFunctionRequest struct {
Name string
Arguments []cty.Value

View File

@ -99,6 +99,10 @@ func (e *fakeHCLExpression) Variables() []hcl.Traversal {
return nil
}
func (e *fakeHCLExpression) Functions() []hcl.Traversal {
return nil
}
func (e *fakeHCLExpression) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return cty.DynamicVal, nil
}

View File

@ -3,55 +3,131 @@ package tofu
import (
"errors"
"fmt"
"log"
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/tfdiags"
"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
// This builds a provider function using an EvalContext and some additional information
// This is split out of BuiltinEvalContext for testing
func evalContextProviderFunction(ctx EvalContext, mc *configs.Config, op walkOperation, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
return func() (providers.Interface, error) {
providerLock.Lock()
defer providerLock.Unlock()
pr, ok := mc.Module.ProviderRequirements.RequiredProviders[pf.ProviderName]
if !ok {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown function provider",
Detail: fmt.Sprintf("Provider %q does not exist within the required_providers of this module", pf.ProviderName),
Subject: rng.ToHCL().Ptr(),
})
}
// Very similar to transform_provider.go
absPc := addrs.AbsProviderConfig{
Provider: pr.Type,
Module: mc.Path,
Alias: pf.ProviderAlias,
}
provider := ctx.Provider(absPc)
if provider == nil {
// Configured provider (NodeApplyableProvider) not required via transform_provider.go. Instead we should use the unconfigured instance (NodeEvalableProvider) in the root.
// Make sure the alias is valid
validAlias := pf.ProviderAlias == ""
if !validAlias {
for _, alias := range pr.Aliases {
if alias.Alias == pf.ProviderAlias {
validAlias = true
break
}
}
if !validAlias {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown function provider",
Detail: fmt.Sprintf("No provider instance %q with alias %q", pf.ProviderName, pf.ProviderAlias),
Subject: rng.ToHCL().Ptr(),
})
}
}
provider = ctx.Provider(addrs.AbsProviderConfig{Provider: pr.Type})
if provider == nil {
log.Printf("[TRACE] tofu.contextFunctions: Initializing function provider %q", addr)
provider, err = factory()
// This should not be possible
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "BUG: Uninitialized function provider",
Detail: fmt.Sprintf("Provider %q has not yet been initialized", absPc.String()),
Subject: rng.ToHCL().Ptr(),
})
}
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))
// First try to look up the function from provider schema
schema := provider.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
return nil, schema.Diagnostics
}
spec, ok := schema.Functions[pf.Function]
if !ok {
// During the validate operation, providers are not configured and therefore won't provide
// a comprehensive GetFunctions list
// Validate is built around unknown values already, we can stub in a placeholder
if op == walkValidate {
// Configured provider functions are not available during validate
fn := function.New(&function.Spec{
Description: "Validate Placeholder",
VarParam: &function.Parameter{
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowUnknown: true,
AllowDynamicType: true,
AllowMarked: false,
},
Type: function.StaticReturnType(cty.DynamicPseudoType),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
return cty.UnknownVal(cty.DynamicPseudoType), nil
},
})
return &fn, nil
}
// The provider may be configured and present additional functions via GetFunctions
specs := provider.GetFunctions()
if specs.Diagnostics.HasErrors() {
return nil, specs.Diagnostics
}
// If the function isn't in the custom GetFunctions list, it must be undefined
spec, ok = specs.Functions[pf.Function]
if !ok {
return nil, diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Function not found in provider",
Detail: fmt.Sprintf("Function %q was not registered by provider %q", pf.Function, absPc.String()),
Subject: rng.ToHCL().Ptr(),
})
}
functions[name] = providerFunction(name, spec, lazy)
}
return functions
fn := providerFunction(pf.Function, spec, provider)
return &fn, nil
}
// 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 {
func providerFunction(name string, spec providers.FunctionSpec, provider providers.Interface) function.Function {
params := make([]function.Parameter, len(spec.Parameters))
for i, param := range spec.Parameters {
params[i] = providerFunctionParameter(param)
@ -64,11 +140,6 @@ func providerFunction(name string, spec providers.FunctionSpec, instance provide
}
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,

View File

@ -1,16 +1,18 @@
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/configs"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
func TestFunctions(t *testing.T) {
@ -105,64 +107,93 @@ func TestFunctions(t *testing.T) {
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
mockProvider.GetFunctionsFn = func() (resp providers.GetFunctionsResponse) {
resp.Functions = mockProvider.GetProviderSchemaResponse.Functions
return resp
}
addr := addrs.NewDefaultProvider("mock")
plugins := newContextPluginsForTest(map[addrs.Provider]providers.Factory{
addr: mockFactory,
}, t)
rng := tfdiags.SourceRange{}
providerFunc := func(fn string) addrs.ProviderFunction {
pf, _ := addrs.ParseFunction(fn).AsProviderFunction()
return pf
}
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")
}
})
mockCtx := new(MockEvalContext)
cfg := &configs.Config{
Module: &configs.Module{
ProviderRequirements: &configs.RequiredProviders{
RequiredProviders: map[string]*configs.RequiredProvider{
"mockname": &configs.RequiredProvider{
Name: "mock",
Type: addr,
},
},
},
},
}
t.Run("broken names map", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected panic due to broken configuration")
}
}()
// Provider missing
_, diags := evalContextProviderFunction(mockCtx, cfg, walkValidate, providerFunc("provider::invalid::unknown"), rng)
if !diags.HasErrors() {
t.Fatal("expected unknown function provider")
}
if diags.Err().Error() != `Unknown function provider: Provider "invalid" does not exist within the required_providers of this module` {
t.Fatal(diags.Err())
}
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")
}
})
// Provider not initialized
_, diags = evalContextProviderFunction(mockCtx, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng)
if !diags.HasErrors() {
t.Fatal("expected unknown function provider")
}
if diags.Err().Error() != `BUG: Uninitialized function provider: Provider "provider[\"registry.opentofu.org/hashicorp/mock\"]" has not yet been initialized` {
t.Fatal(diags.Err())
}
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"])
// "initialize" provider
mockCtx.ProviderProvider = mockProvider
// Function missing (validate)
mockProvider.GetFunctionsCalled = false
_, diags = evalContextProviderFunction(mockCtx, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
if mockProvider.GetFunctionsCalled {
t.Fatal("expected GetFunctions NOT to be called since it's not initialized")
}
// Function missing (Non-validate)
mockProvider.GetFunctionsCalled = false
_, diags = evalContextProviderFunction(mockCtx, cfg, walkPlan, providerFunc("provider::mockname::missing"), rng)
if !diags.HasErrors() {
t.Fatal("expected unknown function")
}
if diags.Err().Error() != `Function not found in provider: Function "missing" was not registered by provider "provider[\"registry.opentofu.org/hashicorp/mock\"]"` {
t.Fatal(diags.Err())
}
if !mockProvider.GetFunctionsCalled {
t.Fatal("expected GetFunctions to be called")
}
ctx := &hcl.EvalContext{
Functions: res.Functions,
Functions: map[string]function.Function{},
Variables: map[string]cty.Value{
"unknown_value": cty.UnknownVal(cty.String),
"sensitive_value": cty.StringVal("sensitive!").Mark(marks.Sensitive),
},
}
// Load functions into ctx
for _, fn := range []string{"echo", "concat", "coalesce", "unknown_param", "error_param"} {
pf := providerFunc("provider::mockname::" + fn)
impl, diags := evalContextProviderFunction(mockCtx, cfg, walkPlan, pf, rng)
if diags.HasErrors() {
t.Fatal(diags.Err())
}
ctx.Functions[pf.String()] = *impl
}
evaluate := func(exprStr string) (cty.Value, hcl.Diagnostics) {
expr, diags := hclsyntax.ParseExpression([]byte(exprStr), "exprtest", hcl.InitialPos)
if diags.HasErrors() {
@ -203,22 +234,14 @@ func TestFunctions(t *testing.T) {
// 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")
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")
}
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())
}
t.Log("Checking sensitive argument")

View File

@ -13,7 +13,6 @@ 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
@ -22,50 +21,14 @@ 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, error) {
ret := &contextPlugins{
return &contextPlugins{
providerFactories: providerFactories,
provisionerFactories: provisionerFactories,
}
// 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
}, nil // TODO remove error from this function call!
}
func (cp *contextPlugins) HasProvider(addr addrs.Provider) bool {
@ -216,26 +179,3 @@ 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
}

View File

@ -13,6 +13,9 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/checks"
"github.com/opentofu/opentofu/internal/configs/configschema"
@ -27,7 +30,6 @@ import (
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/version"
"github.com/zclconf/go-cty/cty"
)
// BuiltinEvalContext is an EvalContext implementation that is used by
@ -70,8 +72,6 @@ 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
@ -90,8 +90,6 @@ 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
}
@ -129,18 +127,16 @@ func (ctx *BuiltinEvalContext) Input() UIInput {
}
func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) {
// If we already initialized, it is an error
if p := ctx.Provider(addr); p != nil {
return nil, fmt.Errorf("%s is already initialized", addr)
}
// Warning: make sure to acquire these locks AFTER the call to Provider
// above, since it also acquires locks.
ctx.ProviderLock.Lock()
defer ctx.ProviderLock.Unlock()
key := addr.String()
// If we have already initialized, it is an error
if _, ok := ctx.ProviderCache[key]; ok {
return nil, fmt.Errorf("%s is already initialized", addr)
}
p, err := ctx.Plugins.NewProviderInstance(addr.Provider)
if err != nil {
return nil, err
@ -526,20 +522,9 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source
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 := ctx.Evaluator.Scope(data, self, source, func(pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
return evalContextProviderFunction(ctx, mc, ctx.Evaluator.Operation, pf, rng)
})
scope.SetActiveExperiments(mc.Module.ActiveExperiments)
return scope

View File

@ -76,21 +76,16 @@ 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, functions *ProviderFunctions) *lang.Scope {
if functions == nil {
functions = new(ProviderFunctions)
}
func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable, functions lang.ProviderFunction) *lang.Scope {
return &lang.Scope{
Data: data,
ParseRef: addrs.ParseRef,
SelfAddr: self,
SourceAddr: source,
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,
Data: data,
ParseRef: addrs.ParseRef,
SelfAddr: self,
SourceAddr: source,
PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval,
BaseDir: ".", // Always current working directory for now.
PlanTimestamp: e.PlanTimestamp,
ProviderFunctions: functions,
}
}

View File

@ -146,6 +146,12 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
// analyze the configuration to find references.
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
// After schema transformer, we can add function references
&ProviderFunctionTransformer{Config: b.Config},
// Remove unused providers and proxies
&PruneProviderTransformer{},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.

View File

@ -89,6 +89,12 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer {
// analyze the configuration to find references.
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
// After schema transformer, we can add function references
&ProviderFunctionTransformer{Config: b.Config},
// Remove unused providers and proxies
&PruneProviderTransformer{},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.

View File

@ -199,6 +199,12 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
// analyze the configuration to find references.
&AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config},
// After schema transformer, we can add function references
&ProviderFunctionTransformer{Config: b.Config},
// Remove unused providers and proxies
&PruneProviderTransformer{},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.

View File

@ -179,6 +179,10 @@ func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal {
return nil
}
func (f fakeHCLExpressionFunc) Functions() []hcl.Traversal {
return nil
}
func (f fakeHCLExpressionFunc) Range() hcl.Range {
return hcl.Range{
Filename: "fake",

View File

@ -87,6 +87,10 @@ type MockProvider struct {
ReadDataSourceRequest providers.ReadDataSourceRequest
ReadDataSourceFn func(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse
GetFunctionsCalled bool
GetFunctionsResponse *providers.GetFunctionsResponse
GetFunctionsFn func() providers.GetFunctionsResponse
CallFunctionCalled bool
CallFunctionResponse *providers.CallFunctionResponse
CallFunctionRequest providers.CallFunctionRequest
@ -521,6 +525,22 @@ func (p *MockProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p
return resp
}
func (p *MockProvider) GetFunctions() (resp providers.GetFunctionsResponse) {
p.Lock()
defer p.Unlock()
p.GetFunctionsCalled = true
if p.GetFunctionsFn != nil {
return p.GetFunctionsFn()
}
if p.GetFunctionsResponse != nil {
resp = *p.GetFunctionsResponse
}
return resp
}
func (p *MockProvider) CallFunction(r providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
p.Lock()
defer p.Unlock()

View File

@ -32,8 +32,11 @@ func transformProviders(concrete ConcreteProviderNodeFunc, config *configs.Confi
&ProviderTransformer{
Config: config,
},
// The following comment shows what must be added to the transformer list after the schema transformer
// After schema transformer, we can add function references
// &ProviderFunctionTransformer{Config: config},
// Remove unused providers and proxies
&PruneProviderTransformer{},
// &PruneProviderTransformer{},
)
}
@ -180,6 +183,7 @@ func (t *ProviderTransformer) Transform(g *Graph) error {
}
if target != nil {
// Providers with configuration will already exist within the graph and can be directly referenced
log.Printf("[TRACE] ProviderTransformer: exact match for %s serving %s", p, dag.VertexName(v))
}
@ -245,6 +249,130 @@ func (t *ProviderTransformer) Transform(g *Graph) error {
return diags.Err()
}
// ProviderFunctionTransformer is a GraphTransformer that maps nodes which reference functions to providers
// within the graph. This will error if there are any provider functions that don't map to known providers.
type ProviderFunctionTransformer struct {
Config *configs.Config
}
func (t *ProviderFunctionTransformer) Transform(g *Graph) error {
var diags tfdiags.Diagnostics
if t.Config == nil {
// This is probably a test case, inherited from ProviderTransformer
log.Printf("[WARN] Skipping provider function transformer due to missing config")
return nil
}
// Locate all providers in the graph
providers := providerVertexMap(g)
type providerReference struct {
path string
name string
alias string
}
// LuT of provider reference -> provider vertex
providerReferences := make(map[providerReference]dag.Vertex)
for _, v := range g.Vertices() {
// Provider function references
if nr, ok := v.(GraphNodeReferencer); ok && t.Config != nil {
for _, ref := range nr.References() {
if pf, ok := ref.Subject.(addrs.ProviderFunction); ok {
key := providerReference{
path: nr.ModulePath().String(),
name: pf.ProviderName,
alias: pf.ProviderAlias,
}
// We already know about this provider and can link directly
if provider, ok := providerReferences[key]; ok {
// Is it worth skipping if we have already connected this provider?
g.Connect(dag.BasicEdge(v, provider))
continue
}
// Find the config that this node belongs to
mc := t.Config.Descendent(nr.ModulePath())
if mc == nil {
// I don't think this is possible
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown Descendent Module",
Detail: nr.ModulePath().String(),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue
}
// Find the provider type from required_providers
pr, ok := mc.Module.ProviderRequirements.RequiredProviders[pf.ProviderName]
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown function provider",
Detail: fmt.Sprintf("Provider %q does not exist within the required_providers of this module", pf.ProviderName),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
continue
}
// Build fully qualified provider address
absPc := addrs.AbsProviderConfig{
Provider: pr.Type,
Module: nr.ModulePath(),
Alias: pf.ProviderAlias,
}
log.Printf("[TRACE] ProviderFunctionTransformer: %s in %s is provided by %s", pf, dag.VertexName(v), absPc)
// Lookup provider via full address
provider := providers[absPc.String()]
if provider != nil {
// Providers with configuration will already exist within the graph and can be directly referenced
log.Printf("[TRACE] ProviderFunctionTransformer: exact match for %s serving %s", absPc, dag.VertexName(v))
} else {
// If this provider doesn't need to be configured then we can just
// stub it out with an init-only provider node, which will just
// start up the provider and fetch its schema.
stubAddr := addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: absPc.Provider,
}
if provider, ok = providers[stubAddr.String()]; !ok {
stub := &NodeEvalableProvider{
&NodeAbstractProvider{
Addr: stubAddr,
},
}
providers[stubAddr.String()] = stub
log.Printf("[TRACE] ProviderFunctionTransformer: creating init-only node for %s", stubAddr)
provider = stub
g.Add(provider)
}
}
// see if this is a proxy provider pointing to another concrete config
if p, ok := provider.(*graphNodeProxyProvider); ok {
g.Remove(p)
provider = p.Target()
}
log.Printf("[DEBUG] ProviderFunctionTransformer: %q (%T) needs %s", dag.VertexName(v), v, dag.VertexName(provider))
g.Connect(dag.BasicEdge(v, provider))
// Save for future lookups
providerReferences[key] = provider
}
}
}
}
return diags.Err()
}
// CloseProviderTransformer is a GraphTransformer that adds nodes to the
// graph that will close open provider connections that aren't needed anymore.
// A provider connection is not needed anymore once all depended resources
@ -279,6 +407,8 @@ func (t *CloseProviderTransformer) Transform(g *Graph) error {
for _, s := range g.UpEdges(p) {
if _, ok := s.(GraphNodeProviderConsumer); ok {
g.Connect(dag.BasicEdge(closer, s))
} else if _, ok := s.(GraphNodeReferencer); ok {
g.Connect(dag.BasicEdge(closer, s))
}
}
}

View File

@ -31,6 +31,30 @@ func testProviderTransformerGraph(t *testing.T, cfg *configs.Config) *Graph {
return g
}
// This variant exists purely for testing and can not currently include the ProviderFunctionTransformer
func testTransformProviders(concrete ConcreteProviderNodeFunc, config *configs.Config) GraphTransformer {
return GraphTransformMulti(
// Add providers from the config
&ProviderConfigTransformer{
Config: config,
Concrete: concrete,
},
// Add any remaining missing providers
&MissingProviderTransformer{
Config: config,
Concrete: concrete,
},
// Connect the providers
&ProviderTransformer{
Config: config,
},
// After schema transformer, we can add function references
// &ProviderFunctionTransformer{Config: config},
// Remove unused providers and proxies
&PruneProviderTransformer{},
)
}
func TestProviderTransformer(t *testing.T) {
mod := testModule(t, "transform-provider-basic")
@ -181,7 +205,7 @@ func TestMissingProviderTransformer_grandchildMissing(t *testing.T) {
g := testProviderTransformerGraph(t, mod)
{
transform := transformProviders(concrete, mod)
transform := testTransformProviders(concrete, mod)
if err := transform.Transform(g); err != nil {
t.Fatalf("err: %s", err)
}
@ -246,7 +270,7 @@ func TestProviderConfigTransformer_parentProviders(t *testing.T) {
g := testProviderTransformerGraph(t, mod)
{
tf := transformProviders(concrete, mod)
tf := testTransformProviders(concrete, mod)
if err := tf.Transform(g); err != nil {
t.Fatalf("err: %s", err)
}
@ -266,7 +290,7 @@ func TestProviderConfigTransformer_grandparentProviders(t *testing.T) {
g := testProviderTransformerGraph(t, mod)
{
tf := transformProviders(concrete, mod)
tf := testTransformProviders(concrete, mod)
if err := tf.Transform(g); err != nil {
t.Fatalf("err: %s", err)
}
@ -300,7 +324,7 @@ resource "test_object" "a" {
g := testProviderTransformerGraph(t, mod)
{
tf := transformProviders(concrete, mod)
tf := testTransformProviders(concrete, mod)
if err := tf.Transform(g); err != nil {
t.Fatalf("err: %s", err)
}
@ -378,7 +402,7 @@ resource "test_object" "a" {
g := testProviderTransformerGraph(t, mod)
{
tf := transformProviders(concrete, mod)
tf := testTransformProviders(concrete, mod)
if err := tf.Transform(g); err != nil {
t.Fatalf("err: %s", err)
}

View File

@ -340,6 +340,8 @@ func (m ReferenceMap) addReference(path addrs.Module, current dag.Vertex, ref *a
subject = ri.ModuleCallOutput()
case addrs.ModuleCallInstance:
subject = ri.Call
case addrs.ProviderFunction:
return nil
default:
log.Printf("[INFO] ReferenceTransformer: reference not found: %q", subject)
return nil
@ -433,6 +435,8 @@ func (m ReferenceMap) dataDependsOn(depender graphNodeDependsOn) []*addrs.Refere
case addrs.ResourceInstance:
resAddr = s.Resource
r.Subject = resAddr
case addrs.ProviderFunction:
continue
}
if resAddr.Mode != addrs.ManagedResourceMode {