github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/refactoring/move_statement.go (about)

     1  package refactoring
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/terraform/internal/addrs"
     7  	"github.com/hashicorp/terraform/internal/configs"
     8  	"github.com/hashicorp/terraform/internal/states"
     9  	"github.com/hashicorp/terraform/internal/tfdiags"
    10  )
    11  
    12  type MoveStatement struct {
    13  	From, To  *addrs.MoveEndpointInModule
    14  	DeclRange tfdiags.SourceRange
    15  
    16  	// Implied is true for statements produced by ImpliedMoveStatements, and
    17  	// false for statements produced by FindMoveStatements.
    18  	//
    19  	// An "implied" statement is one that has no explicit "moved" block in
    20  	// the configuration and was instead generated automatically based on a
    21  	// comparison between current configuration and previous run state.
    22  	// For implied statements, the DeclRange field contains the source location
    23  	// of something in the source code that implied the statement, in which
    24  	// case it would probably be confusing to show that source range to the
    25  	// user, e.g. in an error message, without clearly mentioning that it's
    26  	// related to an implied move statement.
    27  	Implied bool
    28  }
    29  
    30  // FindMoveStatements recurses through the modules of the given configuration
    31  // and returns a flat set of all "moved" blocks defined within, in a
    32  // deterministic but undefined order.
    33  func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
    34  	return findMoveStatements(rootCfg, nil)
    35  }
    36  
    37  func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
    38  	modAddr := cfg.Path
    39  	for _, mc := range cfg.Module.Moved {
    40  		fromAddr, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
    41  		if fromAddr == nil || toAddr == nil {
    42  			// Invalid combination should've been caught during original
    43  			// configuration decoding, in the configs package.
    44  			panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange))
    45  		}
    46  
    47  		into = append(into, MoveStatement{
    48  			From:      fromAddr,
    49  			To:        toAddr,
    50  			DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
    51  			Implied:   false,
    52  		})
    53  	}
    54  
    55  	for _, childCfg := range cfg.Children {
    56  		into = findMoveStatements(childCfg, into)
    57  	}
    58  
    59  	return into
    60  }
    61  
    62  // ImpliedMoveStatements compares addresses in the given state with addresses
    63  // in the given configuration and potentially returns additional MoveStatement
    64  // objects representing moves we infer automatically, even though they aren't
    65  // explicitly recorded in the configuration.
    66  //
    67  // We do this primarily for backward compatibility with behaviors of Terraform
    68  // versions prior to introducing explicit "moved" blocks. Specifically, this
    69  // function aims to achieve the same result as the "NodeCountBoundary"
    70  // heuristic from Terraform v1.0 and earlier, where adding or removing the
    71  // "count" meta-argument from an already-created resource can automatically
    72  // preserve the zeroth or the NoKey instance, depending on the direction of
    73  // the change. We do this only for resources that aren't mentioned already
    74  // in at least one explicit move statement.
    75  //
    76  // As with the previous-version heuristics it replaces, this is a best effort
    77  // and doesn't handle all situations. An explicit move statement is always
    78  // preferred, but our goal here is to match exactly the same cases that the
    79  // old heuristic would've matched, to retain compatibility for existing modules.
    80  //
    81  // We should think very hard before adding any _new_ implication rules for
    82  // moved statements.
    83  func ImpliedMoveStatements(rootCfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement) []MoveStatement {
    84  	return impliedMoveStatements(rootCfg, prevRunState, explicitStmts, nil)
    85  }
    86  
    87  func impliedMoveStatements(cfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement, into []MoveStatement) []MoveStatement {
    88  	modAddr := cfg.Path
    89  
    90  	// There can be potentially many instances of the module, so we need
    91  	// to consider each of them separately.
    92  	for _, modState := range prevRunState.ModuleInstances(modAddr) {
    93  		// What we're looking for here is either a no-key resource instance
    94  		// where the configuration has count set or a zero-key resource
    95  		// instance where the configuration _doesn't_ have count set.
    96  		// If so, we'll generate a statement replacing no-key with zero-key or
    97  		// vice-versa.
    98  		for _, rState := range modState.Resources {
    99  			rAddr := rState.Addr
   100  			rCfg := cfg.Module.ResourceByAddr(rAddr.Resource)
   101  			if rCfg == nil {
   102  				// If there's no configuration at all then there can't be any
   103  				// automatic move fixup to do.
   104  				continue
   105  			}
   106  			approxSrcRange := tfdiags.SourceRangeFromHCL(rCfg.DeclRange)
   107  
   108  			// NOTE: We're intentionally not checking to see whether the
   109  			// "to" addresses in our implied statements already have
   110  			// instances recorded in state, because ApplyMoves should
   111  			// deal with such conflicts in a deterministic way for both
   112  			// explicit and implicit moves, and we'd rather have that
   113  			// handled all in one place.
   114  
   115  			var fromKey, toKey addrs.InstanceKey
   116  
   117  			switch {
   118  			case rCfg.Count != nil:
   119  				// If we have a count expression then we'll use _that_ as
   120  				// a slightly-more-precise approximate source range.
   121  				approxSrcRange = tfdiags.SourceRangeFromHCL(rCfg.Count.Range())
   122  
   123  				if riState := rState.Instances[addrs.NoKey]; riState != nil {
   124  					fromKey = addrs.NoKey
   125  					toKey = addrs.IntKey(0)
   126  				}
   127  			case rCfg.Count == nil && rCfg.ForEach == nil: // no repetition at all
   128  				if riState := rState.Instances[addrs.IntKey(0)]; riState != nil {
   129  					fromKey = addrs.IntKey(0)
   130  					toKey = addrs.NoKey
   131  				}
   132  			}
   133  
   134  			if fromKey != toKey {
   135  				// We mustn't generate an impied statement if the user already
   136  				// wrote an explicit statement referring to this resource,
   137  				// because they may wish to select an instance key other than
   138  				// zero as the one to retain.
   139  				if !haveMoveStatementForResource(rAddr, explicitStmts) {
   140  					into = append(into, MoveStatement{
   141  						From:      addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(fromKey), approxSrcRange),
   142  						To:        addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(toKey), approxSrcRange),
   143  						DeclRange: approxSrcRange,
   144  						Implied:   true,
   145  					})
   146  				}
   147  			}
   148  		}
   149  	}
   150  
   151  	for _, childCfg := range cfg.Children {
   152  		into = impliedMoveStatements(childCfg, prevRunState, explicitStmts, into)
   153  	}
   154  
   155  	return into
   156  }
   157  
   158  func (s *MoveStatement) ObjectKind() addrs.MoveEndpointKind {
   159  	// addrs.UnifyMoveEndpoints guarantees that both of our addresses have
   160  	// the same kind, so we can just arbitrary use From and assume To will
   161  	// match it.
   162  	return s.From.ObjectKind()
   163  }
   164  
   165  // Name is used internally for displaying the statement graph
   166  func (s *MoveStatement) Name() string {
   167  	return fmt.Sprintf("%s->%s", s.From, s.To)
   168  }
   169  
   170  func haveMoveStatementForResource(addr addrs.AbsResource, stmts []MoveStatement) bool {
   171  	// This is not a particularly optimal way to answer this question,
   172  	// particularly since our caller calls this function in a loop already,
   173  	// but we expect the total number of explicit statements to be small
   174  	// in any reasonable Terraform configuration and so a more complicated
   175  	// approach wouldn't be justified here.
   176  
   177  	for _, stmt := range stmts {
   178  		if stmt.From.SelectsResource(addr) {
   179  			return true
   180  		}
   181  		if stmt.To.SelectsResource(addr) {
   182  			return true
   183  		}
   184  	}
   185  	return false
   186  }