core: Evaluate pre/postconditions during validate
During the validation walk, we attempt to proactively evaluate check rule condition and error message expressions. This will help catch some errors as early as possible. At present, resource values in the validation walk are of dynamic type. This means that any references to resources will cause validation to be delayed, rather than presenting useful errors. Validation may still catch other errors, and any future changes which cause better type propagation will result in better validation too.
This commit is contained in:
parent
b06fe04621
commit
b59bffada6
|
@ -2095,3 +2095,293 @@ func TestContext2Validate_nonNullableVariableDefaultValidation(t *testing.T) {
|
||||||
t.Fatal(diags.ErrWithWarnings())
|
t.Fatal(diags.ErrWithWarnings())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_good(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = length(var.input) > 0
|
||||||
|
error_message = "Input cannot be empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatal(diags.ErrWithWarnings())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_badCondition(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = length(one(var.input)) == 1
|
||||||
|
error_message = "You can't do that."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_badErrorMessage(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = var.input != "foo"
|
||||||
|
error_message = "This is a bad use of a function: ${one(var.input)}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_postcondition_good(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = "foo"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = length(self.foo) > 0
|
||||||
|
error_message = "Input cannot be empty."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatal(diags.ErrWithWarnings())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_postcondition_badCondition(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// This postcondition's condition expression does not refer to self, which
|
||||||
|
// is unrealistic. This is because at the time of writing the test, self is
|
||||||
|
// always an unknown value of dynamic type during validation. As a result,
|
||||||
|
// validation of conditions which refer to resource arguments is not
|
||||||
|
// possible until plan time. For now we exercise the code by referring to
|
||||||
|
// an input variable.
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "input" {
|
||||||
|
type = string
|
||||||
|
default = "foo"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = var.input
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = length(one(var.input)) == 1
|
||||||
|
error_message = "You can't do that."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_postcondition_badErrorMessage(t *testing.T) {
|
||||||
|
p := testProvider("aws")
|
||||||
|
p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{
|
||||||
|
ResourceTypes: map[string]*configschema.Block{
|
||||||
|
"aws_instance": {
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"foo": {Type: cty.String, Optional: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
terraform {
|
||||||
|
experiments = [preconditions_postconditions]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
foo = "foo"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = self.foo != "foo"
|
||||||
|
error_message = "This is a bad use of a function: ${one("foo")}."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
diags := ctx.Validate(m)
|
||||||
|
if !diags.HasErrors() {
|
||||||
|
t.Fatalf("succeeded; want error")
|
||||||
|
}
|
||||||
|
if got, want := diags.Err().Error(), "Invalid function argument"; !strings.Contains(got, want) {
|
||||||
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -41,6 +41,18 @@ func (n *NodeValidatableResource) Path() addrs.ModuleInstance {
|
||||||
func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
||||||
diags = diags.Append(n.validateResource(ctx))
|
diags = diags.Append(n.validateResource(ctx))
|
||||||
|
|
||||||
|
var self addrs.Referenceable
|
||||||
|
switch {
|
||||||
|
case n.Config.Count != nil:
|
||||||
|
self = n.Addr.Resource.Instance(addrs.IntKey(0))
|
||||||
|
case n.Config.ForEach != nil:
|
||||||
|
self = n.Addr.Resource.Instance(addrs.StringKey(""))
|
||||||
|
default:
|
||||||
|
self = n.Addr.Resource.Instance(addrs.NoKey)
|
||||||
|
}
|
||||||
|
diags = diags.Append(validateCheckRules(ctx, n.Config.Preconditions, nil))
|
||||||
|
diags = diags.Append(validateCheckRules(ctx, n.Config.Postconditions, self))
|
||||||
|
|
||||||
if managed := n.Config.Managed; managed != nil {
|
if managed := n.Config.Managed; managed != nil {
|
||||||
hasCount := n.Config.Count != nil
|
hasCount := n.Config.Count != nil
|
||||||
hasForEach := n.Config.ForEach != nil
|
hasForEach := n.Config.ForEach != nil
|
||||||
|
@ -466,6 +478,20 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCheckRules(ctx EvalContext, crs []*configs.CheckRule, self addrs.Referenceable) tfdiags.Diagnostics {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
for _, cr := range crs {
|
||||||
|
_, conditionDiags := ctx.EvaluateExpr(cr.Condition, cty.Bool, self)
|
||||||
|
diags = diags.Append(conditionDiags)
|
||||||
|
|
||||||
|
_, errorMessageDiags := ctx.EvaluateExpr(cr.ErrorMessage, cty.String, self)
|
||||||
|
diags = diags.Append(errorMessageDiags)
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
|
||||||
val, countDiags := evaluateCountExpressionValue(expr, ctx)
|
val, countDiags := evaluateCountExpressionValue(expr, ctx)
|
||||||
// If the value isn't known then that's the best we can do for now, but
|
// If the value isn't known then that's the best we can do for now, but
|
||||||
|
|
Loading…
Reference in New Issue