Provider functions in variable validations (#1689)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-05-28 11:05:34 -04:00 committed by GitHub
parent cc8d6e07f4
commit 882b942575
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 88 additions and 10 deletions

View File

@ -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

View File

@ -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")
}

View File

@ -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.

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
}
}