Merge pull request #25261 from hashicorp/jbardin/validate-depends-on
Validate depends_on in modules and outputs
This commit is contained in:
commit
435529a20f
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue