Merge pull request #30645 from hashicorp/alisdair/preconditions-postconditions-expanded-resources
core: Fix expanded condition block validation
This commit is contained in:
commit
2ee64dc7e0
|
@ -2385,3 +2385,97 @@ resource "aws_instance" "test" {
|
||||||
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
t.Errorf("unexpected error.\ngot: %s\nshould contain: %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContext2Validate_precondition_count(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]
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
foos = ["bar", "baz"]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
count = 3
|
||||||
|
foo = local.foos[count.index]
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
precondition {
|
||||||
|
condition = count.index < length(local.foos)
|
||||||
|
error_message = "Insufficient foos."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
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_forEach(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]
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
foos = toset(["bar", "baz", "boop"])
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_instance" "test" {
|
||||||
|
for_each = local.foos
|
||||||
|
foo = "foo"
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
postcondition {
|
||||||
|
condition = length(each.value) == 3
|
||||||
|
error_message = "Short foo required, not \"${each.key}\"."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
"github.com/hashicorp/terraform/internal/configs"
|
"github.com/hashicorp/terraform/internal/configs"
|
||||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||||
"github.com/hashicorp/terraform/internal/didyoumean"
|
"github.com/hashicorp/terraform/internal/didyoumean"
|
||||||
|
"github.com/hashicorp/terraform/internal/instances"
|
||||||
|
"github.com/hashicorp/terraform/internal/lang"
|
||||||
"github.com/hashicorp/terraform/internal/providers"
|
"github.com/hashicorp/terraform/internal/providers"
|
||||||
"github.com/hashicorp/terraform/internal/provisioners"
|
"github.com/hashicorp/terraform/internal/provisioners"
|
||||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||||
|
@ -41,22 +43,9 @@ 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
|
diags = diags.Append(n.validateCheckRules(ctx, n.Config))
|
||||||
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
|
|
||||||
hasForEach := n.Config.ForEach != nil
|
|
||||||
|
|
||||||
// Validate all the provisioners
|
// Validate all the provisioners
|
||||||
for _, p := range managed.Provisioners {
|
for _, p := range managed.Provisioners {
|
||||||
if p.Connection == nil {
|
if p.Connection == nil {
|
||||||
|
@ -66,7 +55,7 @@ func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (di
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Provisioner Config
|
// Validate Provisioner Config
|
||||||
diags = diags.Append(n.validateProvisioner(ctx, p, hasCount, hasForEach))
|
diags = diags.Append(n.validateProvisioner(ctx, p))
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
@ -78,7 +67,7 @@ func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (di
|
||||||
// validateProvisioner validates the configuration of a provisioner belonging to
|
// validateProvisioner validates the configuration of a provisioner belonging to
|
||||||
// a resource. The provisioner config is expected to contain the merged
|
// a resource. The provisioner config is expected to contain the merged
|
||||||
// connection configurations.
|
// connection configurations.
|
||||||
func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *configs.Provisioner, hasCount, hasForEach bool) tfdiags.Diagnostics {
|
func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *configs.Provisioner) tfdiags.Diagnostics {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
provisioner, err := ctx.Provisioner(p.Type)
|
provisioner, err := ctx.Provisioner(p.Type)
|
||||||
|
@ -99,7 +88,7 @@ func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the provisioner's own config first
|
// Validate the provisioner's own config first
|
||||||
configVal, _, configDiags := n.evaluateBlock(ctx, p.Config, provisionerSchema, hasCount, hasForEach)
|
configVal, _, configDiags := n.evaluateBlock(ctx, p.Config, provisionerSchema)
|
||||||
diags = diags.Append(configDiags)
|
diags = diags.Append(configDiags)
|
||||||
|
|
||||||
if configVal == cty.NilVal {
|
if configVal == cty.NilVal {
|
||||||
|
@ -123,42 +112,14 @@ func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *config
|
||||||
// configuration keys that are not valid for *any* communicator, catching
|
// configuration keys that are not valid for *any* communicator, catching
|
||||||
// typos early rather than waiting until we actually try to run one of
|
// typos early rather than waiting until we actually try to run one of
|
||||||
// the resource's provisioners.
|
// the resource's provisioners.
|
||||||
_, _, connDiags := n.evaluateBlock(ctx, p.Connection.Config, connectionBlockSupersetSchema, hasCount, hasForEach)
|
_, _, connDiags := n.evaluateBlock(ctx, p.Connection.Config, connectionBlockSupersetSchema)
|
||||||
diags = diags.Append(connDiags)
|
diags = diags.Append(connDiags)
|
||||||
}
|
}
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NodeValidatableResource) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block, hasCount, hasForEach bool) (cty.Value, hcl.Body, tfdiags.Diagnostics) {
|
func (n *NodeValidatableResource) evaluateBlock(ctx EvalContext, body hcl.Body, schema *configschema.Block) (cty.Value, hcl.Body, tfdiags.Diagnostics) {
|
||||||
keyData := EvalDataForNoInstanceKey
|
keyData, selfAddr := n.stubRepetitionData(n.Config.Count != nil, n.Config.ForEach != nil)
|
||||||
selfAddr := n.ResourceAddr().Resource.Instance(addrs.NoKey)
|
|
||||||
|
|
||||||
if hasCount {
|
|
||||||
// For a resource that has count, we allow count.index but don't
|
|
||||||
// know at this stage what it will return.
|
|
||||||
keyData = InstanceKeyEvalData{
|
|
||||||
CountIndex: cty.UnknownVal(cty.Number),
|
|
||||||
}
|
|
||||||
|
|
||||||
// "self" can't point to an unknown key, but we'll force it to be
|
|
||||||
// key 0 here, which should return an unknown value of the
|
|
||||||
// expected type since none of these elements are known at this
|
|
||||||
// point anyway.
|
|
||||||
selfAddr = n.ResourceAddr().Resource.Instance(addrs.IntKey(0))
|
|
||||||
} else if hasForEach {
|
|
||||||
// For a resource that has for_each, we allow each.value and each.key
|
|
||||||
// but don't know at this stage what it will return.
|
|
||||||
keyData = InstanceKeyEvalData{
|
|
||||||
EachKey: cty.UnknownVal(cty.String),
|
|
||||||
EachValue: cty.DynamicVal,
|
|
||||||
}
|
|
||||||
|
|
||||||
// "self" can't point to an unknown key, but we'll force it to be
|
|
||||||
// key "" here, which should return an unknown value of the
|
|
||||||
// expected type since none of these elements are known at
|
|
||||||
// this point anyway.
|
|
||||||
selfAddr = n.ResourceAddr().Resource.Instance(addrs.StringKey(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.EvaluateBlock(body, schema, selfAddr, keyData)
|
return ctx.EvaluateBlock(body, schema, selfAddr, keyData)
|
||||||
}
|
}
|
||||||
|
@ -478,14 +439,75 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCheckRules(ctx EvalContext, crs []*configs.CheckRule, self addrs.Referenceable) tfdiags.Diagnostics {
|
func (n *NodeValidatableResource) evaluateExpr(ctx EvalContext, expr hcl.Expression, wantTy cty.Type, self addrs.Referenceable, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
for _, cr := range crs {
|
refs, refDiags := lang.ReferencesInExpr(expr)
|
||||||
_, conditionDiags := ctx.EvaluateExpr(cr.Condition, cty.Bool, self)
|
diags = diags.Append(refDiags)
|
||||||
|
|
||||||
|
scope := ctx.EvaluationScope(self, keyData)
|
||||||
|
|
||||||
|
hclCtx, moreDiags := scope.EvalContext(refs)
|
||||||
|
diags = diags.Append(moreDiags)
|
||||||
|
|
||||||
|
result, hclDiags := expr.Value(hclCtx)
|
||||||
|
diags = diags.Append(hclDiags)
|
||||||
|
|
||||||
|
return result, diags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NodeValidatableResource) stubRepetitionData(hasCount, hasForEach bool) (instances.RepetitionData, addrs.Referenceable) {
|
||||||
|
keyData := EvalDataForNoInstanceKey
|
||||||
|
selfAddr := n.ResourceAddr().Resource.Instance(addrs.NoKey)
|
||||||
|
|
||||||
|
if n.Config.Count != nil {
|
||||||
|
// For a resource that has count, we allow count.index but don't
|
||||||
|
// know at this stage what it will return.
|
||||||
|
keyData = InstanceKeyEvalData{
|
||||||
|
CountIndex: cty.UnknownVal(cty.Number),
|
||||||
|
}
|
||||||
|
|
||||||
|
// "self" can't point to an unknown key, but we'll force it to be
|
||||||
|
// key 0 here, which should return an unknown value of the
|
||||||
|
// expected type since none of these elements are known at this
|
||||||
|
// point anyway.
|
||||||
|
selfAddr = n.ResourceAddr().Resource.Instance(addrs.IntKey(0))
|
||||||
|
} else if n.Config.ForEach != nil {
|
||||||
|
// For a resource that has for_each, we allow each.value and each.key
|
||||||
|
// but don't know at this stage what it will return.
|
||||||
|
keyData = InstanceKeyEvalData{
|
||||||
|
EachKey: cty.UnknownVal(cty.String),
|
||||||
|
EachValue: cty.DynamicVal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// "self" can't point to an unknown key, but we'll force it to be
|
||||||
|
// key "" here, which should return an unknown value of the
|
||||||
|
// expected type since none of these elements are known at
|
||||||
|
// this point anyway.
|
||||||
|
selfAddr = n.ResourceAddr().Resource.Instance(addrs.StringKey(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyData, selfAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NodeValidatableResource) validateCheckRules(ctx EvalContext, config *configs.Resource) tfdiags.Diagnostics {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
keyData, selfAddr := n.stubRepetitionData(n.Config.Count != nil, n.Config.ForEach != nil)
|
||||||
|
|
||||||
|
for _, cr := range config.Preconditions {
|
||||||
|
_, conditionDiags := n.evaluateExpr(ctx, cr.Condition, cty.Bool, nil, keyData)
|
||||||
diags = diags.Append(conditionDiags)
|
diags = diags.Append(conditionDiags)
|
||||||
|
|
||||||
_, errorMessageDiags := ctx.EvaluateExpr(cr.ErrorMessage, cty.String, self)
|
_, errorMessageDiags := n.evaluateExpr(ctx, cr.ErrorMessage, cty.Bool, nil, keyData)
|
||||||
|
diags = diags.Append(errorMessageDiags)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cr := range config.Postconditions {
|
||||||
|
_, conditionDiags := n.evaluateExpr(ctx, cr.Condition, cty.Bool, selfAddr, keyData)
|
||||||
|
diags = diags.Append(conditionDiags)
|
||||||
|
|
||||||
|
_, errorMessageDiags := n.evaluateExpr(ctx, cr.ErrorMessage, cty.Bool, selfAddr, keyData)
|
||||||
diags = diags.Append(errorMessageDiags)
|
diags = diags.Append(errorMessageDiags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ func TestNodeValidatableResource_ValidateProvisioner_valid(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
diags := node.validateProvisioner(ctx, pc, false, false)
|
diags := node.validateProvisioner(ctx, pc)
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
t.Fatalf("node.Eval failed: %s", diags.Err())
|
t.Fatalf("node.Eval failed: %s", diags.Err())
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ func TestNodeValidatableResource_ValidateProvisioner__warning(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diags := node.validateProvisioner(ctx, pc, false, false)
|
diags := node.validateProvisioner(ctx, pc)
|
||||||
if len(diags) != 1 {
|
if len(diags) != 1 {
|
||||||
t.Fatalf("wrong number of diagnostics in %s; want one warning", diags.ErrWithWarnings())
|
t.Fatalf("wrong number of diagnostics in %s; want one warning", diags.ErrWithWarnings())
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ func TestNodeValidatableResource_ValidateProvisioner__connectionInvalid(t *testi
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
diags := node.validateProvisioner(ctx, pc, false, false)
|
diags := node.validateProvisioner(ctx, pc)
|
||||||
if !diags.HasErrors() {
|
if !diags.HasErrors() {
|
||||||
t.Fatalf("node.Eval succeeded; want error")
|
t.Fatalf("node.Eval succeeded; want error")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue