cleanup some move graph handling

Create a separate `validateMoveStatementGraph` function so that
`ValidateMoves` and `ApplyMoves` both check the same conditions. Since
we're not using the builtin `graph.Validate` method, because we may have
multiple roots and want better cycle diagnostics, we need to add checks
for self references too. While multiple roots are an error enforced by
`Validate` for the concurrent walk, they are OK when using
`TransitiveReduction` and `ReverseDepthFirstWalk`, so we can skip that
check.

Apply moves must first use `TransitiveReduction` to reduce the graph,
otherwise nodes may be skipped if they are passed over by a transitive
edge.
This commit is contained in:
James Bardin 2021-12-22 17:32:19 -05:00
parent 22dc685052
commit f46cf7b8bc
2 changed files with 62 additions and 24 deletions

View File

@ -31,6 +31,10 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults {
Blocked: make(map[addrs.UniqueKey]MoveBlocked), Blocked: make(map[addrs.UniqueKey]MoveBlocked),
} }
if len(stmts) == 0 {
return ret
}
// The methodology here is to construct a small graph of all of the move // The methodology here is to construct a small graph of all of the move
// statements where the edges represent where a particular statement // statements where the edges represent where a particular statement
// is either chained from or nested inside the effect of another statement. // is either chained from or nested inside the effect of another statement.
@ -39,13 +43,18 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults {
g := buildMoveStatementGraph(stmts) g := buildMoveStatementGraph(stmts)
// If there are any cycles in the graph then we'll not take any action // If the graph is not valid the we will not take any action at all. The
// at all. The separate validation step should detect this and return // separate validation step should detect this and return an error.
// an error. if diags := validateMoveStatementGraph(g); diags.HasErrors() {
if len(g.Cycles()) != 0 { log.Printf("[ERROR] ApplyMoves: %s", diags.ErrWithWarnings())
return ret return ret
} }
// The graph must be reduced in order for ReverseDepthFirstWalk to work
// correctly, since it is built from following edges and can skip over
// dependencies if there is a direct edge to a transitive dependency.
g.TransitiveReduction()
// The starting nodes are the ones that don't depend on any other nodes. // The starting nodes are the ones that don't depend on any other nodes.
startNodes := make(dag.Set, len(stmts)) startNodes := make(dag.Set, len(stmts))
for _, v := range g.Vertices() { for _, v := range g.Vertices() {

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/internal/tfdiags"
) )
@ -31,6 +32,10 @@ import (
func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics { func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
if len(stmts) == 0 {
return diags
}
g := buildMoveStatementGraph(stmts) g := buildMoveStatementGraph(stmts)
// We need to track the absolute versions of our endpoint addresses in // We need to track the absolute versions of our endpoint addresses in
@ -200,6 +205,14 @@ func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts
// validation rules above where we can make better suggestions, and so // validation rules above where we can make better suggestions, and so
// we'll use a cycle report only as a last resort. // we'll use a cycle report only as a last resort.
if !diags.HasErrors() { if !diags.HasErrors() {
diags = diags.Append(validateMoveStatementGraph(g))
}
return diags
}
func validateMoveStatementGraph(g *dag.AcyclicGraph) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, cycle := range g.Cycles() { for _, cycle := range g.Cycles() {
// Reporting cycles is awkward because there isn't any definitive // Reporting cycles is awkward because there isn't any definitive
// way to decide which of the objects in the cycle is the cause of // way to decide which of the objects in the cycle is the cause of
@ -227,6 +240,22 @@ func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts
), ),
)) ))
} }
// Look for cycles to self.
// A user shouldn't be able to create self-references, but we cannot
// correctly process a graph with them.
for _, e := range g.Edges() {
src := e.Source()
if src == e.Target() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Self reference in move statements",
fmt.Sprintf(
"The move statement %s refers to itself the move dependency graph, which is invalid. This is a bug in Terraform; please report it!",
src.(*MoveStatement).Name(),
),
))
}
} }
return diags return diags