mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-27 09:21:14 -06:00
f46cf7b8bc
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.
344 lines
13 KiB
Go
344 lines
13 KiB
Go
package refactoring
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/dag"
|
|
"github.com/hashicorp/terraform/internal/logging"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
)
|
|
|
|
// ApplyMoves modifies in-place the given state object so that any existing
|
|
// objects that are matched by a "from" argument of one of the move statements
|
|
// will be moved to instead appear at the "to" argument of that statement.
|
|
//
|
|
// The result is a map from the unique key of each absolute address that was
|
|
// either the source or destination of a move to a MoveResult describing
|
|
// what happened at that address.
|
|
//
|
|
// ApplyMoves does not have any error situations itself, and will instead just
|
|
// ignore any unresolvable move statements. Validation of a set of moves is
|
|
// a separate concern applied to the configuration, because validity of
|
|
// moves is always dependent only on the configuration, not on the state.
|
|
//
|
|
// ApplyMoves expects exclusive access to the given state while it's running.
|
|
// Don't read or write any part of the state structure until ApplyMoves returns.
|
|
func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults {
|
|
ret := MoveResults{
|
|
Changes: make(map[addrs.UniqueKey]MoveSuccess),
|
|
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
|
|
// statements where the edges represent where a particular statement
|
|
// is either chained from or nested inside the effect of another statement.
|
|
// That then means we can traverse the graph in topological sort order
|
|
// to gradually move objects through potentially multiple moves each.
|
|
|
|
g := buildMoveStatementGraph(stmts)
|
|
|
|
// If the graph is not valid the we will not take any action at all. The
|
|
// separate validation step should detect this and return an error.
|
|
if diags := validateMoveStatementGraph(g); diags.HasErrors() {
|
|
log.Printf("[ERROR] ApplyMoves: %s", diags.ErrWithWarnings())
|
|
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.
|
|
startNodes := make(dag.Set, len(stmts))
|
|
for _, v := range g.Vertices() {
|
|
if len(g.DownEdges(v)) == 0 {
|
|
startNodes.Add(v)
|
|
}
|
|
}
|
|
|
|
if startNodes.Len() == 0 {
|
|
log.Println("[TRACE] refactoring.ApplyMoves: No 'moved' statements to consider in this configuration")
|
|
return ret
|
|
}
|
|
|
|
log.Printf("[TRACE] refactoring.ApplyMoves: Processing 'moved' statements in the configuration\n%s", logging.Indent(g.String()))
|
|
|
|
recordOldAddr := func(oldAddr, newAddr addrs.AbsResourceInstance) {
|
|
oldAddrKey := oldAddr.UniqueKey()
|
|
newAddrKey := newAddr.UniqueKey()
|
|
if prevMove, exists := ret.Changes[oldAddrKey]; exists {
|
|
// If the old address was _already_ the result of a move then
|
|
// we'll replace that entry so that our results summarize a chain
|
|
// of moves into a single entry.
|
|
delete(ret.Changes, oldAddrKey)
|
|
oldAddr = prevMove.From
|
|
}
|
|
ret.Changes[newAddrKey] = MoveSuccess{
|
|
From: oldAddr,
|
|
To: newAddr,
|
|
}
|
|
}
|
|
recordBlockage := func(newAddr, wantedAddr addrs.AbsMoveable) {
|
|
ret.Blocked[newAddr.UniqueKey()] = MoveBlocked{
|
|
Wanted: wantedAddr,
|
|
Actual: newAddr,
|
|
}
|
|
}
|
|
|
|
g.ReverseDepthFirstWalk(startNodes, func(v dag.Vertex, depth int) error {
|
|
stmt := v.(*MoveStatement)
|
|
|
|
for _, ms := range state.Modules {
|
|
modAddr := ms.Addr
|
|
|
|
// We don't yet know that the current module is relevant, and
|
|
// we determine that differently for each the object kind.
|
|
switch kind := stmt.ObjectKind(); kind {
|
|
case addrs.MoveEndpointModule:
|
|
// For a module endpoint we just try the module address
|
|
// directly, and execute the moves if it matches.
|
|
if newAddr, matches := modAddr.MoveDestination(stmt.From, stmt.To); matches {
|
|
log.Printf("[TRACE] refactoring.ApplyMoves: %s has moved to %s", modAddr, newAddr)
|
|
|
|
// If we already have a module at the new address then
|
|
// we'll skip this move and let the existing object take
|
|
// priority.
|
|
if ms := state.Module(newAddr); ms != nil {
|
|
log.Printf("[WARN] Skipped moving %s to %s, because there's already another module instance at the destination", modAddr, newAddr)
|
|
recordBlockage(modAddr, newAddr)
|
|
continue
|
|
}
|
|
|
|
// We need to visit all of the resource instances in the
|
|
// module and record them individually as results.
|
|
for _, rs := range ms.Resources {
|
|
relAddr := rs.Addr.Resource
|
|
for key := range rs.Instances {
|
|
oldInst := relAddr.Instance(key).Absolute(modAddr)
|
|
newInst := relAddr.Instance(key).Absolute(newAddr)
|
|
recordOldAddr(oldInst, newInst)
|
|
}
|
|
}
|
|
|
|
state.MoveModuleInstance(modAddr, newAddr)
|
|
continue
|
|
}
|
|
case addrs.MoveEndpointResource:
|
|
// For a resource endpoint we require an exact containing
|
|
// module match, because by definition a matching resource
|
|
// cannot be nested any deeper than that.
|
|
if !stmt.From.SelectsModule(modAddr) {
|
|
continue
|
|
}
|
|
|
|
// We then need to search each of the resources and resource
|
|
// instances in the module.
|
|
for _, rs := range ms.Resources {
|
|
rAddr := rs.Addr
|
|
if newAddr, matches := rAddr.MoveDestination(stmt.From, stmt.To); matches {
|
|
log.Printf("[TRACE] refactoring.ApplyMoves: resource %s has moved to %s", rAddr, newAddr)
|
|
|
|
// If we already have a resource at the new address then
|
|
// we'll skip this move and let the existing object take
|
|
// priority.
|
|
if rs := state.Resource(newAddr); rs != nil {
|
|
log.Printf("[WARN] Skipped moving %s to %s, because there's already another resource at the destination", rAddr, newAddr)
|
|
recordBlockage(rAddr, newAddr)
|
|
continue
|
|
}
|
|
|
|
for key := range rs.Instances {
|
|
oldInst := rAddr.Instance(key)
|
|
newInst := newAddr.Instance(key)
|
|
recordOldAddr(oldInst, newInst)
|
|
}
|
|
state.MoveAbsResource(rAddr, newAddr)
|
|
continue
|
|
}
|
|
for key := range rs.Instances {
|
|
iAddr := rAddr.Instance(key)
|
|
if newAddr, matches := iAddr.MoveDestination(stmt.From, stmt.To); matches {
|
|
log.Printf("[TRACE] refactoring.ApplyMoves: resource instance %s has moved to %s", iAddr, newAddr)
|
|
|
|
// If we already have a resource instance at the new
|
|
// address then we'll skip this move and let the existing
|
|
// object take priority.
|
|
if is := state.ResourceInstance(newAddr); is != nil {
|
|
log.Printf("[WARN] Skipped moving %s to %s, because there's already another resource instance at the destination", iAddr, newAddr)
|
|
recordBlockage(iAddr, newAddr)
|
|
continue
|
|
}
|
|
|
|
recordOldAddr(iAddr, newAddr)
|
|
|
|
state.MoveAbsResourceInstance(iAddr, newAddr)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
panic(fmt.Sprintf("unhandled move object kind %s", kind))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return ret
|
|
}
|
|
|
|
// buildMoveStatementGraph constructs a dependency graph of the given move
|
|
// statements, where the nodes are all pointers to statements in the given
|
|
// slice and the edges represent either chaining or nesting relationships.
|
|
//
|
|
// buildMoveStatementGraph doesn't do any validation of the graph, so it
|
|
// may contain cycles and other sorts of invalidity.
|
|
func buildMoveStatementGraph(stmts []MoveStatement) *dag.AcyclicGraph {
|
|
g := &dag.AcyclicGraph{}
|
|
for i := range stmts {
|
|
// The graph nodes are pointers to the actual statements directly.
|
|
g.Add(&stmts[i])
|
|
}
|
|
|
|
// Now we'll add the edges representing chaining and nesting relationships.
|
|
// We assume that a reasonable configuration will have at most tens of
|
|
// move statements and thus this N*M algorithm is acceptable.
|
|
for dependerI := range stmts {
|
|
depender := &stmts[dependerI]
|
|
for dependeeI := range stmts {
|
|
if dependerI == dependeeI {
|
|
// skip comparing the statement to itself
|
|
continue
|
|
}
|
|
dependee := &stmts[dependeeI]
|
|
|
|
if statementDependsOn(depender, dependee) {
|
|
g.Connect(dag.BasicEdge(depender, dependee))
|
|
}
|
|
}
|
|
}
|
|
|
|
return g
|
|
}
|
|
|
|
// statementDependsOn returns true if statement a depends on statement b;
|
|
// i.e. statement b must be executed before statement a.
|
|
func statementDependsOn(a, b *MoveStatement) bool {
|
|
// chain-able moves are simple, as on the destination of one move could be
|
|
// equal to the source of another.
|
|
if a.From.CanChainFrom(b.To) {
|
|
return true
|
|
}
|
|
|
|
// Statement nesting in more complex, as we have 8 possible combinations to
|
|
// assess. Here we list all combinations, along with the statement which
|
|
// must be executed first when one address is nested within another.
|
|
// A.From IsNestedWithin B.From => A
|
|
// A.From IsNestedWithin B.To => B
|
|
// A.To IsNestedWithin B.From => A
|
|
// A.To IsNestedWithin B.To => B
|
|
// B.From IsNestedWithin A.From => B
|
|
// B.From IsNestedWithin A.To => A
|
|
// B.To IsNestedWithin A.From => B
|
|
// B.To IsNestedWithin A.To => A
|
|
//
|
|
// Since we are only interested in checking if A depends on B, we only need
|
|
// to check the 4 possibilities above which result in B being executed
|
|
// first. If we're there's no dependency at all we can return immediately.
|
|
if !(a.From.NestedWithin(b.To) || a.To.NestedWithin(b.To) ||
|
|
b.From.NestedWithin(a.From) || b.To.NestedWithin(a.From)) {
|
|
return false
|
|
}
|
|
|
|
// If a nested move has a dependency, we need to rule out the possibility
|
|
// that this is a move inside a module only changing indexes. If an
|
|
// ancestor module is only changing the index of a nested module, any
|
|
// nested move statements are going to match both the From and To address
|
|
// when the base name is not changing, causing a cycle in the order of
|
|
// operations.
|
|
|
|
// if A is not declared in an ancestor module, then we can't be nested
|
|
// within a module index change.
|
|
if len(a.To.Module()) >= len(b.To.Module()) {
|
|
return true
|
|
}
|
|
// We only want the nested move statement to depend on the outer module
|
|
// move, so we only test this in the reverse direction.
|
|
if a.From.IsModuleReIndex(a.To) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// MoveResults describes the outcome of an ApplyMoves call.
|
|
type MoveResults struct {
|
|
// Changes is a map from the unique keys of the final new resource
|
|
// instance addresses to an object describing what changed.
|
|
//
|
|
// This includes one entry for each resource instance address that was
|
|
// the destination of a move statement. It doesn't include resource
|
|
// instances that were not affected by moves at all, but it does include
|
|
// resource instance addresses that were "blocked" (also recorded in
|
|
// BlockedAddrs) if and only if they were able to move at least
|
|
// partially along a chain before being blocked.
|
|
//
|
|
// In the return value from ApplyMoves, all of the keys are guaranteed to
|
|
// be unique keys derived from addrs.AbsResourceInstance values.
|
|
Changes map[addrs.UniqueKey]MoveSuccess
|
|
|
|
// Blocked is a map from the unique keys of the final new
|
|
// resource instances addresses to information about where they "wanted"
|
|
// to move, but were blocked by a pre-existing object at the same address.
|
|
//
|
|
// "Blocking" can arise in unusual situations where multiple points along
|
|
// a move chain were already bound to objects, and thus only one of them
|
|
// can actually adopt the final position in the chain. It can also
|
|
// occur in other similar situations, such as if a configuration contains
|
|
// a move of an entire module and a move of an individual resource into
|
|
// that module, such that the individual resource would collide with a
|
|
// resource in the whole module that was moved.
|
|
//
|
|
// In the return value from ApplyMoves, all of the keys are guaranteed to
|
|
// be unique keys derived from values of addrs.AbsMoveable types.
|
|
Blocked map[addrs.UniqueKey]MoveBlocked
|
|
}
|
|
|
|
type MoveSuccess struct {
|
|
From addrs.AbsResourceInstance
|
|
To addrs.AbsResourceInstance
|
|
}
|
|
|
|
type MoveBlocked struct {
|
|
Wanted addrs.AbsMoveable
|
|
Actual addrs.AbsMoveable
|
|
}
|
|
|
|
// AddrMoved returns true if and only if the given resource instance moved to
|
|
// a new address in the ApplyMoves call that the receiver is describing.
|
|
//
|
|
// If AddrMoved returns true, you can pass the same address to method OldAddr
|
|
// to find its original address prior to moving.
|
|
func (rs MoveResults) AddrMoved(newAddr addrs.AbsResourceInstance) bool {
|
|
_, ok := rs.Changes[newAddr.UniqueKey()]
|
|
return ok
|
|
}
|
|
|
|
// OldAddr returns the old address of the given resource instance address, or
|
|
// just returns back the same address if the given instance wasn't affected by
|
|
// any move statements.
|
|
func (rs MoveResults) OldAddr(newAddr addrs.AbsResourceInstance) addrs.AbsResourceInstance {
|
|
change, ok := rs.Changes[newAddr.UniqueKey()]
|
|
if !ok {
|
|
return newAddr
|
|
}
|
|
return change.From
|
|
}
|