Merge pull request #30253 from hashicorp/jbardin/move-graph

cleanup some move graph handling
This commit is contained in:
James Bardin 2022-01-04 12:51:12 -05:00 committed by GitHub
commit 8bbba22f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,30 +205,54 @@ 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() {
for _, cycle := range g.Cycles() { diags = diags.Append(validateMoveStatementGraph(g))
// Reporting cycles is awkward because there isn't any definitive }
// way to decide which of the objects in the cycle is the cause of
// the problem. Therefore we'll just list them all out and leave
// the user to figure it out. :(
stmtStrs := make([]string, 0, len(cycle))
for _, stmtI := range cycle {
// move statement graph nodes are pointers to move statements
stmt := stmtI.(*MoveStatement)
stmtStrs = append(stmtStrs, fmt.Sprintf(
"\n - %s: %s → %s",
stmt.DeclRange.StartString(),
stmt.From.String(),
stmt.To.String(),
))
}
sort.Strings(stmtStrs) // just to make the order deterministic
return diags
}
func validateMoveStatementGraph(g *dag.AcyclicGraph) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, cycle := range g.Cycles() {
// Reporting cycles is awkward because there isn't any definitive
// way to decide which of the objects in the cycle is the cause of
// the problem. Therefore we'll just list them all out and leave
// the user to figure it out. :(
stmtStrs := make([]string, 0, len(cycle))
for _, stmtI := range cycle {
// move statement graph nodes are pointers to move statements
stmt := stmtI.(*MoveStatement)
stmtStrs = append(stmtStrs, fmt.Sprintf(
"\n - %s: %s → %s",
stmt.DeclRange.StartString(),
stmt.From.String(),
stmt.To.String(),
))
}
sort.Strings(stmtStrs) // just to make the order deterministic
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cyclic dependency in move statements",
fmt.Sprintf(
"The following chained move statements form a cycle, and so there is no final location to move objects to:%s\n\nA chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.",
strings.Join(stmtStrs, ""),
),
))
}
// 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( diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error, tfdiags.Error,
"Cyclic dependency in move statements", "Self reference in move statements",
fmt.Sprintf( fmt.Sprintf(
"The following chained move statements form a cycle, and so there is no final location to move objects to:%s\n\nA chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.", "The move statement %s refers to itself the move dependency graph, which is invalid. This is a bug in Terraform; please report it!",
strings.Join(stmtStrs, ""), src.(*MoveStatement).Name(),
), ),
)) ))
} }