diff --git a/CHANGELOG.md b/CHANGELOG.md index abb4c40d28..2c156fc1de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ BUG FIXES: * Fixed support for provider functions in tests ([#1603](https://github.com/opentofu/opentofu/pull/1603)) * Added a better error message on `for_each` block with sensitive value of unsuitable type. ([#1485](https://github.com/opentofu/opentofu/pull/1485)) * Fix race condition on locking in gcs backend ([#1342](https://github.com/opentofu/opentofu/pull/1342)) +* Fix bug where provider functions were unusable in variables and outputs ([#1689](https://github.com/opentofu/opentofu/pull/1689)) ## Previous Releases diff --git a/internal/command/e2etest/testdata/functions/main.tf b/internal/command/e2etest/testdata/functions/main.tf index 73ffae356c..21da4e66f8 100644 --- a/internal/command/e2etest/testdata/functions/main.tf +++ b/internal/command/e2etest/testdata/functions/main.tf @@ -7,6 +7,15 @@ terraform { } } +variable "number" { + type = number + default = 1 + validation { + condition = provider::example::echo(var.number) > 0 + error_message = "number must be > ${provider::example::echo(0)}" + } +} + output "dummy" { value = provider::example::echo("Hello Functions") } diff --git a/internal/lang/eval.go b/internal/lang/eval.go index e3e5f8fc4a..bfe8b3258e 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/function" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs/configschema" @@ -147,6 +148,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem ctx := &hcl.EvalContext{ Variables: vals, + // TODO consider if any provider functions make sense here Functions: s.Functions(), } @@ -297,11 +299,14 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl var diags tfdiags.Diagnostics vals := make(map[string]cty.Value) - funcs := s.Functions() + funcs := make(map[string]function.Function) ctx := &hcl.EvalContext{ Variables: vals, Functions: funcs, } + for name, fn := range s.Functions() { + funcs[name] = fn + } if len(refs) == 0 { // Easy path for common case where there are no references at all. diff --git a/internal/lang/references.go b/internal/lang/references.go index 6a43f8acde..d6a7dbf874 100644 --- a/internal/lang/references.go +++ b/internal/lang/references.go @@ -92,6 +92,20 @@ func ReferencesInExpr(parseRef ParseRef, expr hcl.Expression) ([]*addrs.Referenc return References(parseRef, traversals) } +// ProviderFunctionsInExpr is a helper wrapper around References that searches for provider +// function traversals in an ExpressionWithFunctions, then converts the traversals into +// references +func ProviderFunctionsInExpr(parseRef ParseRef, expr hcl.Expression) ([]*addrs.Reference, tfdiags.Diagnostics) { + if expr == nil { + return nil, nil + } + if fexpr, ok := expr.(hcl.ExpressionWithFunctions); ok { + funcs := filterProviderFunctions(fexpr.Functions()) + return References(parseRef, funcs) + } + return nil, nil +} + func filterProviderFunctions(funcs []hcl.Traversal) []hcl.Traversal { pfuncs := make([]hcl.Traversal, 0, len(funcs)) for _, fn := range funcs { diff --git a/internal/tofu/eval_variable.go b/internal/tofu/eval_variable.go index 198cb36ff9..1033033db2 100644 --- a/internal/tofu/eval_variable.go +++ b/internal/tofu/eval_variable.go @@ -18,6 +18,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/checks" "github.com/opentofu/opentofu/internal/configs" + "github.com/opentofu/opentofu/internal/lang" "github.com/opentofu/opentofu/internal/lang/marks" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -234,16 +235,25 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config }) return diags } - hclCtx := &hcl.EvalContext{ - Variables: map[string]cty.Value{ - "var": cty.ObjectVal(map[string]cty.Value{ - config.Name: val, - }), - }, - Functions: ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey).Functions(), - } - for ix, validation := range config.Validations { + condFuncs, condDiags := lang.ProviderFunctionsInExpr(addrs.ParseRef, validation.Condition) + diags = diags.Append(condDiags) + errFuncs, errDiags := lang.ProviderFunctionsInExpr(addrs.ParseRef, validation.ErrorMessage) + diags = diags.Append(errDiags) + + if diags.HasErrors() { + continue + } + + hclCtx, ctxDiags := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey).EvalContext(append(condFuncs, errFuncs...)) + diags = diags.Append(ctxDiags) + if diags.HasErrors() { + continue + } + hclCtx.Variables["var"] = cty.ObjectVal(map[string]cty.Value{ + config.Name: val, + }) + result, ruleDiags := evalVariableValidation(validation, hclCtx, addr, config, expr, ix) diags = diags.Append(ruleDiags) diff --git a/internal/tofu/node_module_variable.go b/internal/tofu/node_module_variable.go index 8f759e5429..77022faa2b 100644 --- a/internal/tofu/node_module_variable.go +++ b/internal/tofu/node_module_variable.go @@ -167,6 +167,23 @@ func (n *nodeModuleVariable) ModulePath() addrs.Module { return n.Addr.Module.Module() } +// GraphNodeReferencer +func (n *nodeModuleVariable) References() []*addrs.Reference { + // This is identical to NodeRootVariable.References + var refs []*addrs.Reference + + if n.Config != nil { + for _, validation := range n.Config.Validations { + condFuncs, _ := lang.ProviderFunctionsInExpr(addrs.ParseRef, validation.Condition) + refs = append(refs, condFuncs...) + errFuncs, _ := lang.ProviderFunctionsInExpr(addrs.ParseRef, validation.ErrorMessage) + refs = append(refs, errFuncs...) + } + } + + return refs +} + // GraphNodeExecutable func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { log.Printf("[TRACE] nodeModuleVariable: evaluating %s", n.Addr) diff --git a/internal/tofu/node_root_variable.go b/internal/tofu/node_root_variable.go index 65f66fa0eb..9f3e791657 100644 --- a/internal/tofu/node_root_variable.go +++ b/internal/tofu/node_root_variable.go @@ -13,6 +13,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/dag" + "github.com/opentofu/opentofu/internal/lang" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -52,6 +53,23 @@ func (n *NodeRootVariable) ReferenceableAddrs() []addrs.Referenceable { return []addrs.Referenceable{n.Addr} } +// GraphNodeReferencer +func (n *NodeRootVariable) References() []*addrs.Reference { + // This is identical to nodeModuleVariable.References + var refs []*addrs.Reference + + if n.Config != nil { + for _, validation := range n.Config.Validations { + condFuncs, _ := lang.ProviderFunctionsInExpr(addrs.ParseRef, validation.Condition) + refs = append(refs, condFuncs...) + errFuncs, _ := lang.ProviderFunctionsInExpr(addrs.ParseRef, validation.ErrorMessage) + refs = append(refs, errFuncs...) + } + } + + return refs +} + // GraphNodeExecutable func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { // Root module variables are special in that they are provided directly diff --git a/internal/tofu/transform_destroy_edge.go b/internal/tofu/transform_destroy_edge.go index 7167f46a89..c2445c60ef 100644 --- a/internal/tofu/transform_destroy_edge.go +++ b/internal/tofu/transform_destroy_edge.go @@ -359,11 +359,15 @@ func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error { // earlier, however there may be more to prune now based on // targeting or a destroy with no related instances in the // state. + // TODO: consider replacing this with an actual "references" check instead of the simple type check below. + // Due to provider functions, many provider references through GraphNodeReferencer still are required. des, _ := g.Descendents(n) for _, v := range des { switch v.(type) { case GraphNodeProviderConsumer: return + case GraphNodeReferencer: + return } }