Skip to content

Commit

Permalink
terraform: refactor nodeModuleVariable and NodeRootVariable EvalTree(…
Browse files Browse the repository at this point in the history
…)s (#26245)

EvalModuleCallArguments is now a method on nodeModuleVariable, it's only
caller, and the other functions have been replaces with straight through
code (or in the case of evalVariableValidations, a standalone function).

I was unable to add tests for nodeModuleVariable.Execute, which requires
fixtures that aren't part of the MockEvalContext (a scope.evalContext is
one); it's not ideal but that function should be well covered by the
context tests so I chose to leave it as-is.

Finally, I removed the unused function hclTypeName. Deleting code is
fun!
  • Loading branch information
mildwonkey committed Sep 16, 2020
1 parent 5c69cf0 commit f64d5b2
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 202 deletions.
166 changes: 15 additions & 151 deletions terraform/eval_variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,138 +3,25 @@ package terraform
import (
"fmt"
"log"
"reflect"

"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/instances"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)

// EvalSetModuleCallArguments is an EvalNode implementation that sets values
// for arguments of a child module call, for later retrieval during
// expression evaluation.
type EvalSetModuleCallArguments struct {
Module addrs.ModuleCallInstance
Values map[string]cty.Value
}

// TODO: test
func (n *EvalSetModuleCallArguments) Eval(ctx EvalContext) (interface{}, error) {
ctx.SetModuleCallArguments(n.Module, n.Values)
return nil, nil
}

// EvalModuleCallArgument is an EvalNode implementation that produces the value
// for a particular variable as will be used by a child module instance.
//
// The result is written into the map given in Values, with its key
// set to the local name of the variable, disregarding the module instance
// address. Any existing values in that map are deleted first. This weird
// interface is a result of trying to be convenient for use with
// EvalContext.SetModuleCallArguments, which expects a map to merge in with
// any existing arguments.
type EvalModuleCallArgument struct {
Addr addrs.InputVariable
Config *configs.Variable
Expr hcl.Expression
ModuleInstance addrs.ModuleInstance

Values map[string]cty.Value

// validateOnly indicates that this evaluation is only for config
// validation, and we will not have any expansion module instance
// repetition data.
validateOnly bool
}

func (n *EvalModuleCallArgument) Eval(ctx EvalContext) (interface{}, error) {
// Clear out the existing mapping
for k := range n.Values {
delete(n.Values, k)
}

wantType := n.Config.Type
name := n.Addr.Name
expr := n.Expr

if expr == nil {
// Should never happen, but we'll bail out early here rather than
// crash in case it does. We set no value at all in this case,
// making a subsequent call to EvalContext.SetModuleCallArguments
// a no-op.
log.Printf("[ERROR] attempt to evaluate %s with nil expression", n.Addr.String())
return nil, nil
}

var moduleInstanceRepetitionData instances.RepetitionData

switch {
case n.validateOnly:
// the instance expander does not track unknown expansion values, so we
// have to assume all RepetitionData is unknown.
moduleInstanceRepetitionData = instances.RepetitionData{
CountIndex: cty.UnknownVal(cty.Number),
EachKey: cty.UnknownVal(cty.String),
EachValue: cty.DynamicVal,
}

default:
// Get the repetition data for this module instance,
// so we can create the appropriate scope for evaluating our expression
moduleInstanceRepetitionData = ctx.InstanceExpander().GetModuleInstanceRepetitionData(n.ModuleInstance)
}

scope := ctx.EvaluationScope(nil, moduleInstanceRepetitionData)
val, diags := scope.EvalExpr(expr, cty.DynamicPseudoType)

// We intentionally passed DynamicPseudoType to EvalExpr above because
// now we can do our own local type conversion and produce an error message
// with better context if it fails.
var convErr error
val, convErr = convert.Convert(val, wantType)
if convErr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid value for module argument",
Detail: fmt.Sprintf(
"The given value is not suitable for child module variable %q defined at %s: %s.",
name, n.Config.DeclRange.String(), convErr,
),
Subject: expr.Range().Ptr(),
})
// We'll return a placeholder unknown value to avoid producing
// redundant downstream errors.
val = cty.UnknownVal(wantType)
}

n.Values[name] = val
return nil, diags.ErrWithWarnings()
}

// evalVariableValidations is an EvalNode implementation that ensures that
// all of the configured custom validations for a variable are passing.
// evalVariableValidations ensures ta all of the configured custom validations
// for a variable are passing.
//
// This must be used only after any side-effects that make the value of the
// variable available for use in expression evaluation, such as
// EvalModuleCallArgument for variables in descendent modules.
type evalVariableValidations struct {
Addr addrs.AbsInputVariableInstance
Config *configs.Variable

// Expr is the expression that provided the value for the variable, if any.
// This will be nil for root module variables, because their values come
// from outside the configuration.
Expr hcl.Expression
}

func (n *evalVariableValidations) Eval(ctx EvalContext) (interface{}, error) {
if n.Config == nil || len(n.Config.Validations) == 0 {
log.Printf("[TRACE] evalVariableValidations: not active for %s, so skipping", n.Addr)
return nil, nil
func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ctx EvalContext) error {
if config == nil || len(config.Validations) == 0 {
log.Printf("[TRACE] evalVariableValidations: not active for %s, so skipping", addr)
return nil
}

var diags tfdiags.Diagnostics
Expand All @@ -148,27 +35,27 @@ func (n *evalVariableValidations) Eval(ctx EvalContext) (interface{}, error) {
// bypass our usual evaluation machinery here and just produce a minimal
// evaluation context containing just the required value, and thus avoid
// the problem that ctx's evaluation functions refer to the wrong module.
val := ctx.GetVariableValue(n.Addr)
val := ctx.GetVariableValue(addr)
hclCtx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{
n.Config.Name: val,
config.Name: val,
}),
},
Functions: ctx.EvaluationScope(nil, EvalDataForNoInstanceKey).Functions(),
}

for _, validation := range n.Config.Validations {
for _, validation := range config.Validations {
const errInvalidCondition = "Invalid variable validation result"
const errInvalidValue = "Invalid value for variable"

result, moreDiags := validation.Condition.Value(hclCtx)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition expression failed: %s", n.Addr, validation.DeclRange, diags.Err().Error())
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition expression failed: %s", addr, validation.DeclRange, diags.Err().Error())
}
if !result.IsKnown() {
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", n.Addr, validation.DeclRange)
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange)
continue // We'll wait until we've learned more, then.
}
if result.IsNull() {
Expand Down Expand Up @@ -197,12 +84,12 @@ func (n *evalVariableValidations) Eval(ctx EvalContext) (interface{}, error) {
}

if result.False() {
if n.Expr != nil {
if expr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: errInvalidValue,
Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", validation.ErrorMessage, validation.DeclRange.String()),
Subject: n.Expr.Range().Ptr(),
Subject: expr.Range().Ptr(),
})
} else {
// Since we don't have a source expression for a root module
Expand All @@ -212,34 +99,11 @@ func (n *evalVariableValidations) Eval(ctx EvalContext) (interface{}, error) {
Severity: hcl.DiagError,
Summary: errInvalidValue,
Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", validation.ErrorMessage, validation.DeclRange.String()),
Subject: n.Config.DeclRange.Ptr(),
Subject: config.DeclRange.Ptr(),
})
}
}
}

return nil, diags.ErrWithWarnings()
}

// hclTypeName returns the name of the type that would represent this value in
// a config file, or falls back to the Go type name if there's no corresponding
// HCL type. This is used for formatted output, not for comparing types.
func hclTypeName(i interface{}) string {
switch k := reflect.Indirect(reflect.ValueOf(i)).Kind(); k {
case reflect.Bool:
return "boolean"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64:
return "number"
case reflect.Array, reflect.Slice:
return "list"
case reflect.Map:
return "map"
case reflect.String:
return "string"
default:
// fall back to the Go type if there's no match
return k.String()
}
return diags.ErrWithWarnings()
}

0 comments on commit f64d5b2

Please sign in to comment.