mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
238 lines
8.4 KiB
Go
238 lines
8.4 KiB
Go
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/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
|
|
|
|
// If this flag is set, any diagnostics are discarded and this operation
|
|
// will always succeed, though may produce an unknown value in the
|
|
// event of an error.
|
|
IgnoreDiagnostics bool
|
|
|
|
Values map[string]cty.Value
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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 EvaluateExpr 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
|
|
if n.IgnoreDiagnostics {
|
|
return nil, nil
|
|
}
|
|
return nil, diags.ErrWithWarnings()
|
|
}
|
|
|
|
// evalVariableValidations is an EvalNode implementation that ensures that
|
|
// 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
|
|
|
|
// If this flag is set, this node becomes a no-op.
|
|
// This is here for consistency with EvalModuleCallArgument so that it
|
|
// can be populated with the same value, where needed.
|
|
IgnoreDiagnostics bool
|
|
}
|
|
|
|
func (n *evalVariableValidations) Eval(ctx EvalContext) (interface{}, error) {
|
|
if n.Config == nil || n.IgnoreDiagnostics || len(n.Config.Validations) == 0 {
|
|
log.Printf("[TRACE] evalVariableValidations: not active for %s, so skipping", n.Addr)
|
|
return nil, nil
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// Variable nodes evaluate in the parent module to where they were declared
|
|
// because the value expression (n.Expr, if set) comes from the calling
|
|
// "module" block in the parent module.
|
|
//
|
|
// Validation expressions are statically validated (during configuration
|
|
// loading) to refer only to the variable being validated, so we can
|
|
// 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)
|
|
hclCtx := &hcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"var": cty.ObjectVal(map[string]cty.Value{
|
|
n.Config.Name: val,
|
|
}),
|
|
},
|
|
Functions: ctx.EvaluationScope(nil, EvalDataForNoInstanceKey).Functions(),
|
|
}
|
|
|
|
for _, validation := range n.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())
|
|
}
|
|
if !result.IsKnown() {
|
|
log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", n.Addr, validation.DeclRange)
|
|
continue // We'll wait until we've learned more, then.
|
|
}
|
|
if result.IsNull() {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: errInvalidCondition,
|
|
Detail: "Validation condition expression must return either true or false, not null.",
|
|
Subject: validation.Condition.Range().Ptr(),
|
|
Expression: validation.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
continue
|
|
}
|
|
var err error
|
|
result, err = convert.Convert(result, cty.Bool)
|
|
if err != nil {
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: errInvalidCondition,
|
|
Detail: fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)),
|
|
Subject: validation.Condition.Range().Ptr(),
|
|
Expression: validation.Condition,
|
|
EvalContext: hclCtx,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if result.False() {
|
|
if n.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(),
|
|
})
|
|
} else {
|
|
// Since we don't have a source expression for a root module
|
|
// variable, we'll just report the error from the perspective
|
|
// of the variable declaration itself.
|
|
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.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()
|
|
}
|
|
}
|