opentofu/internal/tofu/context_functions.go
Christian Mesh 015b79b139
Allow unconfigured provider functions in test context (#1603)
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
2024-05-07 09:31:17 -04:00

181 lines
5.8 KiB
Go

package tofu
import (
"errors"
"fmt"
"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"
)
// This builds a provider function using an EvalContext and some additional information
// This is split out of BuiltinEvalContext for testing
func evalContextProviderFunction(providers func(addrs.AbsProviderConfig) providers.Interface, mc *configs.Config, op walkOperation, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
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 := providers(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 = providers(addrs.AbsProviderConfig{Provider: pr.Type})
if provider == nil {
// 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(),
})
}
}
// 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(),
})
}
}
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, provider providers.Interface) 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) {
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,
}
}