github.com/hugorut/terraform@v1.1.3/src/refactoring/move_statement.go (about) 1 package refactoring 2 3 import ( 4 "fmt" 5 6 "github.com/hugorut/terraform/src/addrs" 7 "github.com/hugorut/terraform/src/configs" 8 "github.com/hugorut/terraform/src/states" 9 "github.com/hugorut/terraform/src/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 = findMoveStatements(childCfg, 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 }