github.com/opentofu/opentofu@v1.7.1/internal/refactoring/move_execute.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package refactoring
     7  
     8  import (
     9  	"fmt"
    10  	"log"
    11  
    12  	"github.com/opentofu/opentofu/internal/addrs"
    13  	"github.com/opentofu/opentofu/internal/dag"
    14  	"github.com/opentofu/opentofu/internal/logging"
    15  	"github.com/opentofu/opentofu/internal/states"
    16  )
    17  
    18  // ApplyMoves modifies in-place the given state object so that any existing
    19  // objects that are matched by a "from" argument of one of the move statements
    20  // will be moved to instead appear at the "to" argument of that statement.
    21  //
    22  // The result is a map from the unique key of each absolute address that was
    23  // either the source or destination of a move to a MoveResult describing
    24  // what happened at that address.
    25  //
    26  // ApplyMoves does not have any error situations itself, and will instead just
    27  // ignore any unresolvable move statements. Validation of a set of moves is
    28  // a separate concern applied to the configuration, because validity of
    29  // moves is always dependent only on the configuration, not on the state.
    30  //
    31  // ApplyMoves expects exclusive access to the given state while it's running.
    32  // Don't read or write any part of the state structure until ApplyMoves returns.
    33  func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults {
    34  	ret := makeMoveResults()
    35  
    36  	if len(stmts) == 0 {
    37  		return ret
    38  	}
    39  
    40  	// The methodology here is to construct a small graph of all of the move
    41  	// statements where the edges represent where a particular statement
    42  	// is either chained from or nested inside the effect of another statement.
    43  	// That then means we can traverse the graph in topological sort order
    44  	// to gradually move objects through potentially multiple moves each.
    45  
    46  	g := buildMoveStatementGraph(stmts)
    47  
    48  	// If the graph is not valid the we will not take any action at all. The
    49  	// separate validation step should detect this and return an error.
    50  	if diags := validateMoveStatementGraph(g); diags.HasErrors() {
    51  		log.Printf("[ERROR] ApplyMoves: %s", diags.ErrWithWarnings())
    52  		return ret
    53  	}
    54  
    55  	// The graph must be reduced in order for ReverseDepthFirstWalk to work
    56  	// correctly, since it is built from following edges and can skip over
    57  	// dependencies if there is a direct edge to a transitive dependency.
    58  	g.TransitiveReduction()
    59  
    60  	// The starting nodes are the ones that don't depend on any other nodes.
    61  	startNodes := make(dag.Set, len(stmts))
    62  	for _, v := range g.Vertices() {
    63  		if len(g.DownEdges(v)) == 0 {
    64  			startNodes.Add(v)
    65  		}
    66  	}
    67  
    68  	if startNodes.Len() == 0 {
    69  		log.Println("[TRACE] refactoring.ApplyMoves: No 'moved' statements to consider in this configuration")
    70  		return ret
    71  	}
    72  
    73  	log.Printf("[TRACE] refactoring.ApplyMoves: Processing 'moved' statements in the configuration\n%s", logging.Indent(g.String()))
    74  
    75  	recordOldAddr := func(oldAddr, newAddr addrs.AbsResourceInstance) {
    76  		if prevMove, exists := ret.Changes.GetOk(oldAddr); exists {
    77  			// If the old address was _already_ the result of a move then
    78  			// we'll replace that entry so that our results summarize a chain
    79  			// of moves into a single entry.
    80  			ret.Changes.Remove(oldAddr)
    81  			oldAddr = prevMove.From
    82  		}
    83  		ret.Changes.Put(newAddr, MoveSuccess{
    84  			From: oldAddr,
    85  			To:   newAddr,
    86  		})
    87  	}
    88  	recordBlockage := func(newAddr, wantedAddr addrs.AbsMoveable) {
    89  		ret.Blocked.Put(newAddr, MoveBlocked{
    90  			Wanted: wantedAddr,
    91  			Actual: newAddr,
    92  		})
    93  	}
    94  
    95  	for _, v := range g.ReverseTopologicalOrder() {
    96  		stmt := v.(*MoveStatement)
    97  
    98  		for _, ms := range state.Modules {
    99  			modAddr := ms.Addr
   100  
   101  			// We don't yet know that the current module is relevant, and
   102  			// we determine that differently for each the object kind.
   103  			switch kind := stmt.ObjectKind(); kind {
   104  			case addrs.MoveEndpointModule:
   105  				// For a module endpoint we just try the module address
   106  				// directly, and execute the moves if it matches.
   107  				if newAddr, matches := modAddr.MoveDestination(stmt.From, stmt.To); matches {
   108  					log.Printf("[TRACE] refactoring.ApplyMoves: %s has moved to %s", modAddr, newAddr)
   109  
   110  					// If we already have a module at the new address then
   111  					// we'll skip this move and let the existing object take
   112  					// priority.
   113  					if ms := state.Module(newAddr); ms != nil {
   114  						log.Printf("[WARN] Skipped moving %s to %s, because there's already another module instance at the destination", modAddr, newAddr)
   115  						recordBlockage(modAddr, newAddr)
   116  						continue
   117  					}
   118  
   119  					// We need to visit all of the resource instances in the
   120  					// module and record them individually as results.
   121  					for _, rs := range ms.Resources {
   122  						relAddr := rs.Addr.Resource
   123  						for key := range rs.Instances {
   124  							oldInst := relAddr.Instance(key).Absolute(modAddr)
   125  							newInst := relAddr.Instance(key).Absolute(newAddr)
   126  							recordOldAddr(oldInst, newInst)
   127  						}
   128  					}
   129  
   130  					state.MoveModuleInstance(modAddr, newAddr)
   131  					continue
   132  				}
   133  			case addrs.MoveEndpointResource:
   134  				// For a resource endpoint we require an exact containing
   135  				// module match, because by definition a matching resource
   136  				// cannot be nested any deeper than that.
   137  				if !stmt.From.SelectsModule(modAddr) {
   138  					continue
   139  				}
   140  
   141  				// We then need to search each of the resources and resource
   142  				// instances in the module.
   143  				for _, rs := range ms.Resources {
   144  					rAddr := rs.Addr
   145  					if newAddr, matches := rAddr.MoveDestination(stmt.From, stmt.To); matches {
   146  						log.Printf("[TRACE] refactoring.ApplyMoves: resource %s has moved to %s", rAddr, newAddr)
   147  
   148  						// If we already have a resource at the new address then
   149  						// we'll skip this move and let the existing object take
   150  						// priority.
   151  						if rs := state.Resource(newAddr); rs != nil {
   152  							log.Printf("[WARN] Skipped moving %s to %s, because there's already another resource at the destination", rAddr, newAddr)
   153  							recordBlockage(rAddr, newAddr)
   154  							continue
   155  						}
   156  
   157  						for key := range rs.Instances {
   158  							oldInst := rAddr.Instance(key)
   159  							newInst := newAddr.Instance(key)
   160  							recordOldAddr(oldInst, newInst)
   161  						}
   162  						state.MoveAbsResource(rAddr, newAddr)
   163  						continue
   164  					}
   165  					for key := range rs.Instances {
   166  						iAddr := rAddr.Instance(key)
   167  						if newAddr, matches := iAddr.MoveDestination(stmt.From, stmt.To); matches {
   168  							log.Printf("[TRACE] refactoring.ApplyMoves: resource instance %s has moved to %s", iAddr, newAddr)
   169  
   170  							// If we already have a resource instance at the new
   171  							// address then we'll skip this move and let the existing
   172  							// object take priority.
   173  							if is := state.ResourceInstance(newAddr); is != nil {
   174  								log.Printf("[WARN] Skipped moving %s to %s, because there's already another resource instance at the destination", iAddr, newAddr)
   175  								recordBlockage(iAddr, newAddr)
   176  								continue
   177  							}
   178  
   179  							recordOldAddr(iAddr, newAddr)
   180  
   181  							state.MoveAbsResourceInstance(iAddr, newAddr)
   182  							continue
   183  						}
   184  					}
   185  				}
   186  			default:
   187  				panic(fmt.Sprintf("unhandled move object kind %s", kind))
   188  			}
   189  		}
   190  	}
   191  
   192  	return ret
   193  }
   194  
   195  // buildMoveStatementGraph constructs a dependency graph of the given move
   196  // statements, where the nodes are all pointers to statements in the given
   197  // slice and the edges represent either chaining or nesting relationships.
   198  //
   199  // buildMoveStatementGraph doesn't do any validation of the graph, so it
   200  // may contain cycles and other sorts of invalidity.
   201  func buildMoveStatementGraph(stmts []MoveStatement) *dag.AcyclicGraph {
   202  	g := &dag.AcyclicGraph{}
   203  	for i := range stmts {
   204  		// The graph nodes are pointers to the actual statements directly.
   205  		g.Add(&stmts[i])
   206  	}
   207  
   208  	// Now we'll add the edges representing chaining and nesting relationships.
   209  	// We assume that a reasonable configuration will have at most tens of
   210  	// move statements and thus this N*M algorithm is acceptable.
   211  	for dependerI := range stmts {
   212  		depender := &stmts[dependerI]
   213  		for dependeeI := range stmts {
   214  			if dependerI == dependeeI {
   215  				// skip comparing the statement to itself
   216  				continue
   217  			}
   218  			dependee := &stmts[dependeeI]
   219  
   220  			if statementDependsOn(depender, dependee) {
   221  				g.Connect(dag.BasicEdge(depender, dependee))
   222  			}
   223  		}
   224  	}
   225  
   226  	return g
   227  }
   228  
   229  // statementDependsOn returns true if statement a depends on statement b;
   230  // i.e. statement b must be executed before statement a.
   231  func statementDependsOn(a, b *MoveStatement) bool {
   232  	// chain-able moves are simple, as on the destination of one move could be
   233  	// equal to the source of another.
   234  	if a.From.CanChainFrom(b.To) {
   235  		return true
   236  	}
   237  
   238  	// Statement nesting in more complex, as we have 8 possible combinations to
   239  	// assess. Here we list all combinations, along with the statement which
   240  	// must be executed first when one address is nested within another.
   241  	// A.From  IsNestedWithin  B.From => A
   242  	// A.From  IsNestedWithin  B.To   => B
   243  	// A.To    IsNestedWithin  B.From => A
   244  	// A.To    IsNestedWithin  B.To   => B
   245  	// B.From  IsNestedWithin  A.From => B
   246  	// B.From  IsNestedWithin  A.To   => A
   247  	// B.To    IsNestedWithin  A.From => B
   248  	// B.To    IsNestedWithin  A.To   => A
   249  	//
   250  	// Since we are only interested in checking if A depends on B, we only need
   251  	// to check the 4 possibilities above which result in B being executed
   252  	// first. If we're there's no dependency at all we can return immediately.
   253  	if !(a.From.NestedWithin(b.To) || a.To.NestedWithin(b.To) ||
   254  		b.From.NestedWithin(a.From) || b.To.NestedWithin(a.From)) {
   255  		return false
   256  	}
   257  
   258  	// If a nested move has a dependency, we need to rule out the possibility
   259  	// that this is a move inside a module only changing indexes. If an
   260  	// ancestor module is only changing the index of a nested module, any
   261  	// nested move statements are going to match both the From and To address
   262  	// when the base name is not changing, causing a cycle in the order of
   263  	// operations.
   264  
   265  	// if A is not declared in an ancestor module, then we can't be nested
   266  	// within a module index change.
   267  	if len(a.To.Module()) >= len(b.To.Module()) {
   268  		return true
   269  	}
   270  	// We only want the nested move statement to depend on the outer module
   271  	// move, so we only test this in the reverse direction.
   272  	if a.From.IsModuleReIndex(a.To) {
   273  		return false
   274  	}
   275  
   276  	return true
   277  }
   278  
   279  // MoveResults describes the outcome of an ApplyMoves call.
   280  type MoveResults struct {
   281  	// Changes is a map from the unique keys of the final new resource
   282  	// instance addresses to an object describing what changed.
   283  	//
   284  	// This includes one entry for each resource instance address that was
   285  	// the destination of a move statement. It doesn't include resource
   286  	// instances that were not affected by moves at all, but it does include
   287  	// resource instance addresses that were "blocked" (also recorded in
   288  	// BlockedAddrs) if and only if they were able to move at least
   289  	// partially along a chain before being blocked.
   290  	//
   291  	// In the return value from ApplyMoves, all of the keys are guaranteed to
   292  	// be unique keys derived from addrs.AbsResourceInstance values.
   293  	Changes addrs.Map[addrs.AbsResourceInstance, MoveSuccess]
   294  
   295  	// Blocked is a map from the unique keys of the final new
   296  	// resource instances addresses to information about where they "wanted"
   297  	// to move, but were blocked by a pre-existing object at the same address.
   298  	//
   299  	// "Blocking" can arise in unusual situations where multiple points along
   300  	// a move chain were already bound to objects, and thus only one of them
   301  	// can actually adopt the final position in the chain. It can also
   302  	// occur in other similar situations, such as if a configuration contains
   303  	// a move of an entire module and a move of an individual resource into
   304  	// that module, such that the individual resource would collide with a
   305  	// resource in the whole module that was moved.
   306  	//
   307  	// In the return value from ApplyMoves, all of the keys are guaranteed to
   308  	// be unique keys derived from values of addrs.AbsMoveable types.
   309  	Blocked addrs.Map[addrs.AbsMoveable, MoveBlocked]
   310  }
   311  
   312  func makeMoveResults() MoveResults {
   313  	return MoveResults{
   314  		Changes: addrs.MakeMap[addrs.AbsResourceInstance, MoveSuccess](),
   315  		Blocked: addrs.MakeMap[addrs.AbsMoveable, MoveBlocked](),
   316  	}
   317  }
   318  
   319  type MoveSuccess struct {
   320  	From addrs.AbsResourceInstance
   321  	To   addrs.AbsResourceInstance
   322  }
   323  
   324  type MoveBlocked struct {
   325  	Wanted addrs.AbsMoveable
   326  	Actual addrs.AbsMoveable
   327  }
   328  
   329  // AddrMoved returns true if and only if the given resource instance moved to
   330  // a new address in the ApplyMoves call that the receiver is describing.
   331  //
   332  // If AddrMoved returns true, you can pass the same address to method OldAddr
   333  // to find its original address prior to moving.
   334  func (rs MoveResults) AddrMoved(newAddr addrs.AbsResourceInstance) bool {
   335  	return rs.Changes.Has(newAddr)
   336  }
   337  
   338  // OldAddr returns the old address of the given resource instance address, or
   339  // just returns back the same address if the given instance wasn't affected by
   340  // any move statements.
   341  func (rs MoveResults) OldAddr(newAddr addrs.AbsResourceInstance) addrs.AbsResourceInstance {
   342  	change, ok := rs.Changes.GetOk(newAddr)
   343  	if !ok {
   344  		return newAddr
   345  	}
   346  	return change.From
   347  }