mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
b06fe04621
Error messages for preconditions, postconditions, and custom variable validations have until now been string literals. This commit changes this to treat the field as an HCL expression, which must evaluate to a string. Most commonly this will either be a string literal or a template expression. When the check rule condition is evaluated, we also evaluate the error message. This means that the error message should always evaluate to a string value, even if the condition passes. If it does not, this will result in an error diagnostic. If the condition fails, and the error message also fails to evaluate, we fall back to a default error message. This means that the check rule failure will still be reported, alongside diagnostics explaining why the custom error message failed to render. As part of this change, we also necessarily remove the heuristic about the error message format. This guidance can be readded in future as part of a configuration hint system.
168 lines
5.5 KiB
Go
168 lines
5.5 KiB
Go
package terraform
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hcltest"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
)
|
|
|
|
func TestNodeRootVariableExecute(t *testing.T) {
|
|
t.Run("type conversion", func(t *testing.T) {
|
|
ctx := new(MockEvalContext)
|
|
|
|
n := &NodeRootVariable{
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
|
Config: &configs.Variable{
|
|
Name: "foo",
|
|
Type: cty.String,
|
|
ConstraintType: cty.String,
|
|
},
|
|
RawValue: &InputValue{
|
|
Value: cty.True,
|
|
SourceType: ValueFromUnknown,
|
|
},
|
|
}
|
|
|
|
diags := n.Execute(ctx, walkApply)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected error: %s", diags.Err())
|
|
}
|
|
|
|
if !ctx.SetRootModuleArgumentCalled {
|
|
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
|
|
}
|
|
if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
|
|
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if got, want := ctx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) {
|
|
// NOTE: The given value was cty.Bool but the type constraint was
|
|
// cty.String, so it was NodeRootVariable's responsibility to convert
|
|
// as part of preparing the "final value".
|
|
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
t.Run("validation", func(t *testing.T) {
|
|
ctx := new(MockEvalContext)
|
|
|
|
// The variable validation function gets called with Terraform's
|
|
// built-in functions available, so we need a minimal scope just for
|
|
// it to get the functions from.
|
|
ctx.EvaluationScopeScope = &lang.Scope{}
|
|
|
|
// We need to reimplement a _little_ bit of EvalContextBuiltin logic
|
|
// here to get a similar effect with EvalContextMock just to get the
|
|
// value to flow through here in a realistic way that'll make this test
|
|
// useful.
|
|
var finalVal cty.Value
|
|
ctx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) {
|
|
if addr.Name == "foo" {
|
|
t.Logf("set %s to %#v", addr.String(), v)
|
|
finalVal = v
|
|
}
|
|
}
|
|
ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
|
|
if addr.String() != "var.foo" {
|
|
return cty.NilVal
|
|
}
|
|
t.Logf("reading final val for %s (%#v)", addr.String(), finalVal)
|
|
return finalVal
|
|
}
|
|
|
|
n := &NodeRootVariable{
|
|
Addr: addrs.InputVariable{Name: "foo"},
|
|
Config: &configs.Variable{
|
|
Name: "foo",
|
|
Type: cty.Number,
|
|
ConstraintType: cty.Number,
|
|
Validations: []*configs.CheckRule{
|
|
{
|
|
Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
// This returns true only if the given variable value
|
|
// is exactly cty.Number, which allows us to verify
|
|
// that we were given the value _after_ type
|
|
// conversion.
|
|
// This had previously not been handled correctly,
|
|
// as reported in:
|
|
// https://github.com/hashicorp/terraform/issues/29899
|
|
vars := ctx.Variables["var"]
|
|
if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") {
|
|
t.Logf("var.foo isn't available")
|
|
return cty.False, nil
|
|
}
|
|
val := vars.GetAttr("foo")
|
|
if val == cty.NilVal || val.Type() != cty.Number {
|
|
t.Logf("var.foo is %#v; want a number", val)
|
|
return cty.False, nil
|
|
}
|
|
return cty.True, nil
|
|
}),
|
|
ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")),
|
|
},
|
|
},
|
|
},
|
|
RawValue: &InputValue{
|
|
// Note: This is a string, but the variable's type constraint
|
|
// is number so it should be converted before use.
|
|
Value: cty.StringVal("5"),
|
|
SourceType: ValueFromUnknown,
|
|
},
|
|
}
|
|
|
|
diags := n.Execute(ctx, walkApply)
|
|
if diags.HasErrors() {
|
|
t.Fatalf("unexpected error: %s", diags.Err())
|
|
}
|
|
|
|
if !ctx.SetRootModuleArgumentCalled {
|
|
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
|
|
}
|
|
if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
|
|
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
|
|
}
|
|
if got, want := ctx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) {
|
|
// NOTE: The given value was cty.Bool but the type constraint was
|
|
// cty.String, so it was NodeRootVariable's responsibility to convert
|
|
// as part of preparing the "final value".
|
|
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
// fakeHCLExpressionFunc is a fake implementation of hcl.Expression that just
|
|
// directly produces a value with direct Go code.
|
|
//
|
|
// An expression of this type has no references and so it cannot access any
|
|
// variables from the EvalContext unless something else arranges for them
|
|
// to be guaranteed available. For example, custom variable validations just
|
|
// unconditionally have access to the variable they are validating regardless
|
|
// of references.
|
|
type fakeHCLExpressionFunc func(*hcl.EvalContext) (cty.Value, hcl.Diagnostics)
|
|
|
|
var _ hcl.Expression = fakeHCLExpressionFunc(nil)
|
|
|
|
func (f fakeHCLExpressionFunc) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
|
|
return f(ctx)
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal {
|
|
return nil
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) Range() hcl.Range {
|
|
return hcl.Range{
|
|
Filename: "fake",
|
|
Start: hcl.InitialPos,
|
|
End: hcl.InitialPos,
|
|
}
|
|
}
|
|
|
|
func (f fakeHCLExpressionFunc) StartRange() hcl.Range {
|
|
return f.Range()
|
|
}
|