Merge pull request #25261 from hashicorp/jbardin/validate-depends-on

Validate depends_on in modules and outputs
This commit is contained in:
James Bardin 2020-06-16 13:56:50 -04:00 committed by GitHub
commit 435529a20f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 137 additions and 43 deletions

View File

@ -1632,3 +1632,83 @@ output "out" {
t.Fatal(diags.ErrWithWarnings()) t.Fatal(diags.ErrWithWarnings())
} }
} }
func TestContext2Validate_invalidModuleDependsOn(t *testing.T) {
// validate module and output depends_on
m := testModuleInline(t, map[string]string{
"main.tf": `
module "mod1" {
source = "./mod"
depends_on = [resource_foo.bar.baz]
}
module "mod2" {
source = "./mod"
depends_on = [resource_foo.bar.baz]
}
`,
"mod/main.tf": `
output "out" {
value = "foo"
}
`,
})
diags := testContext2(t, &ContextOpts{
Config: m,
}).Validate()
if !diags.HasErrors() {
t.Fatal("succeeded; want errors")
}
if len(diags) != 2 {
t.Fatalf("wanted 2 diagnostic errors, got %q", diags)
}
for _, d := range diags {
des := d.Description().Summary
if !strings.Contains(des, "Invalid depends_on reference") {
t.Fatalf(`expected "Invalid depends_on reference", got %q`, des)
}
}
}
func TestContext2Validate_invalidOutputDependsOn(t *testing.T) {
// validate module and output depends_on
m := testModuleInline(t, map[string]string{
"main.tf": `
module "mod1" {
source = "./mod"
}
output "out" {
value = "bar"
depends_on = [resource_foo.bar.baz]
}
`,
"mod/main.tf": `
output "out" {
value = "bar"
depends_on = [resource_foo.bar.baz]
}
`,
})
diags := testContext2(t, &ContextOpts{
Config: m,
}).Validate()
if !diags.HasErrors() {
t.Fatal("succeeded; want errors")
}
if len(diags) != 2 {
t.Fatalf("wanted 2 diagnostic errors, got %q", diags)
}
for _, d := range diags {
des := d.Description().Summary
if !strings.Contains(des, "Invalid depends_on reference") {
t.Fatalf(`expected "Invalid depends_on reference", got %q`, des)
}
}
}

View File

@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
) )
@ -33,8 +33,7 @@ func (n *EvalDeleteOutput) Eval(ctx EvalContext) (interface{}, error) {
// for the given name to the current state. // for the given name to the current state.
type EvalWriteOutput struct { type EvalWriteOutput struct {
Addr addrs.OutputValue Addr addrs.OutputValue
Sensitive bool Config *configs.Output
Expr hcl.Expression
// ContinueOnErr allows interpolation to fail during Input // ContinueOnErr allows interpolation to fail during Input
ContinueOnErr bool ContinueOnErr bool
} }
@ -45,9 +44,13 @@ func (n *EvalWriteOutput) Eval(ctx EvalContext) (interface{}, error) {
// This has to run before we have a state lock, since evaluation also // This has to run before we have a state lock, since evaluation also
// reads the state // reads the state
val, diags := ctx.EvaluateExpr(n.Expr, cty.DynamicPseudoType, nil) val, diags := ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)
// We'll handle errors below, after we have loaded the module. // We'll handle errors below, after we have loaded the module.
// Outputs don't have a separate mode for validation, so validate
// depends_on expressions here too
diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
state := ctx.State() state := ctx.State()
if state == nil { if state == nil {
return nil, nil return nil, nil
@ -80,7 +83,7 @@ func (n *EvalWriteOutput) setValue(addr addrs.AbsOutputValue, state *states.Sync
// changeset below, if we have one on this graph walk. // changeset below, if we have one on this graph walk.
log.Printf("[TRACE] EvalWriteOutput: Saving value for %s in state", addr) log.Printf("[TRACE] EvalWriteOutput: Saving value for %s in state", addr)
stateVal := cty.UnknownAsNull(val) stateVal := cty.UnknownAsNull(val)
state.SetOutputValue(addr, stateVal, n.Sensitive) state.SetOutputValue(addr, stateVal, n.Config.Sensitive)
} else { } else {
log.Printf("[TRACE] EvalWriteOutput: Removing %s from state (it is now null)", addr) log.Printf("[TRACE] EvalWriteOutput: Removing %s from state (it is now null)", addr)
state.RemoveOutputValue(addr) state.RemoveOutputValue(addr)
@ -100,7 +103,7 @@ func (n *EvalWriteOutput) setValue(addr addrs.AbsOutputValue, state *states.Sync
if !val.IsNull() { if !val.IsNull() {
change = &plans.OutputChange{ change = &plans.OutputChange{
Addr: addr, Addr: addr,
Sensitive: n.Sensitive, Sensitive: n.Config.Sensitive,
Change: plans.Change{ Change: plans.Change{
Action: plans.Create, Action: plans.Create,
Before: cty.NullVal(cty.DynamicPseudoType), Before: cty.NullVal(cty.DynamicPseudoType),
@ -110,7 +113,7 @@ func (n *EvalWriteOutput) setValue(addr addrs.AbsOutputValue, state *states.Sync
} else { } else {
change = &plans.OutputChange{ change = &plans.OutputChange{
Addr: addr, Addr: addr,
Sensitive: n.Sensitive, Sensitive: n.Config.Sensitive,
Change: plans.Change{ Change: plans.Change{
// This is just a weird placeholder delete action since // This is just a weird placeholder delete action since
// we don't have an actual prior value to indicate. // we don't have an actual prior value to indicate.

View File

@ -3,6 +3,7 @@ package terraform
import ( import (
"testing" "testing"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
@ -44,6 +45,7 @@ func TestEvalWriteMapOutput(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
evalNode := &EvalWriteOutput{ evalNode := &EvalWriteOutput{
Config: &configs.Output{},
Addr: addrs.OutputValue{Name: tc.name}, Addr: addrs.OutputValue{Name: tc.name},
} }
ctx.EvaluateExprResult = tc.val ctx.EvaluateExprResult = tc.val

View File

@ -401,29 +401,7 @@ func (n *EvalValidateResource) Eval(ctx EvalContext) (interface{}, error) {
diags = diags.Append(forEachDiags) diags = diags.Append(forEachDiags)
} }
for _, traversal := range n.Config.DependsOn { diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn))
ref, refDiags := addrs.ParseRef(traversal)
diags = diags.Append(refDiags)
if !refDiags.HasErrors() && len(ref.Remaining) != 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid depends_on reference",
Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.",
Subject: ref.Remaining.SourceRange().Ptr(),
})
}
// The ref must also refer to something that exists. To test that,
// we'll just eval it and count on the fact that our evaluator will
// detect references to non-existent objects.
if !diags.HasErrors() {
scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey)
if scope != nil { // sometimes nil in tests, due to incomplete mocks
_, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType)
diags = diags.Append(refDiags)
}
}
}
// Validate the provider_meta block for the provider this resource // Validate the provider_meta block for the provider this resource
// belongs to, if there is one. // belongs to, if there is one.
@ -622,3 +600,30 @@ func (n *EvalValidateResource) validateForEach(ctx EvalContext, expr hcl.Express
return diags return diags
} }
func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiags.Diagnostics) {
for _, traversal := range dependsOn {
ref, refDiags := addrs.ParseRef(traversal)
diags = diags.Append(refDiags)
if !refDiags.HasErrors() && len(ref.Remaining) != 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid depends_on reference",
Detail: "References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.",
Subject: ref.Remaining.SourceRange().Ptr(),
})
}
// The ref must also refer to something that exists. To test that,
// we'll just eval it and count on the fact that our evaluator will
// detect references to non-existent objects.
if !diags.HasErrors() {
scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey)
if scope != nil { // sometimes nil in tests, due to incomplete mocks
_, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType)
diags = diags.Append(refDiags)
}
}
}
return diags
}

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/dag" "github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/lang" "github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/tfdiags"
) )
type ConcreteModuleNodeFunc func(n *nodeExpandModule) dag.Vertex type ConcreteModuleNodeFunc func(n *nodeExpandModule) dag.Vertex
@ -271,6 +272,7 @@ type evalValidateModule struct {
func (n *evalValidateModule) Eval(ctx EvalContext) (interface{}, error) { func (n *evalValidateModule) Eval(ctx EvalContext) (interface{}, error) {
_, call := n.Addr.Call() _, call := n.Addr.Call()
var diags tfdiags.Diagnostics
expander := ctx.InstanceExpander() expander := ctx.InstanceExpander()
// Modules all evaluate to single instances during validation, only to // Modules all evaluate to single instances during validation, only to
@ -285,20 +287,23 @@ func (n *evalValidateModule) Eval(ctx EvalContext) (interface{}, error) {
// a full expansion, presuming these errors will be caught in later steps // a full expansion, presuming these errors will be caught in later steps
switch { switch {
case n.ModuleCall.Count != nil: case n.ModuleCall.Count != nil:
_, diags := evaluateCountExpressionValue(n.ModuleCall.Count, ctx) _, countDiags := evaluateCountExpressionValue(n.ModuleCall.Count, ctx)
if diags.HasErrors() { diags = diags.Append(countDiags)
return nil, diags.Err()
}
case n.ModuleCall.ForEach != nil: case n.ModuleCall.ForEach != nil:
_, diags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx) _, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx)
if diags.HasErrors() { diags = diags.Append(forEachDiags)
return nil, diags.Err()
}
} }
diags = diags.Append(validateDependsOn(ctx, n.ModuleCall.DependsOn))
// now set our own mode to single // now set our own mode to single
expander.SetModuleSingle(module, call) expander.SetModuleSingle(module, call)
} }
if diags.HasErrors() {
return nil, diags.ErrWithWarnings()
}
return nil, nil return nil, nil
} }

View File

@ -229,8 +229,7 @@ func (n *NodeApplyableOutput) EvalTree() EvalNode {
Ops: []walkOperation{walkEval, walkRefresh, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy}, Ops: []walkOperation{walkEval, walkRefresh, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy},
Node: &EvalWriteOutput{ Node: &EvalWriteOutput{
Addr: n.Addr.OutputValue, Addr: n.Addr.OutputValue,
Sensitive: n.Config.Sensitive, Config: n.Config,
Expr: n.Config.Expr,
}, },
}, },
}, },