github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/refactoring/move_validate.go (about) 1 package refactoring 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/hashicorp/terraform/internal/addrs" 10 "github.com/hashicorp/terraform/internal/configs" 11 "github.com/hashicorp/terraform/internal/instances" 12 "github.com/hashicorp/terraform/internal/tfdiags" 13 ) 14 15 // ValidateMoves tests whether all of the given move statements comply with 16 // both the single-statement validation rules and the "big picture" rules 17 // that constrain statements in relation to one another. 18 // 19 // The validation rules are primarily in terms of the configuration, but 20 // ValidateMoves also takes the expander that resulted from creating a plan 21 // so that it can see which instances are defined for each module and resource, 22 // to precisely validate move statements involving specific-instance addresses. 23 // 24 // Because validation depends on the planning result but move execution must 25 // happen _before_ planning, we have the unusual situation where sibling 26 // function ApplyMoves must run before ValidateMoves and must therefore 27 // tolerate and ignore any invalid statements. The plan walk will then 28 // construct in incorrect plan (because it'll be starting from the wrong 29 // prior state) but ValidateMoves will block actually showing that invalid 30 // plan to the user. 31 func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics { 32 var diags tfdiags.Diagnostics 33 34 g := buildMoveStatementGraph(stmts) 35 36 // We need to track the absolute versions of our endpoint addresses in 37 // order to detect when there are ambiguous moves. 38 type AbsMoveEndpoint struct { 39 Other addrs.AbsMoveable 40 StmtRange tfdiags.SourceRange 41 } 42 stmtFrom := map[addrs.UniqueKey]AbsMoveEndpoint{} 43 stmtTo := map[addrs.UniqueKey]AbsMoveEndpoint{} 44 45 for _, stmt := range stmts { 46 // Earlier code that constructs MoveStatement values should ensure that 47 // both stmt.From and stmt.To always belong to the same statement and 48 // thus to the same module. 49 stmtMod, fromCallSteps := stmt.From.ModuleCallTraversals() 50 _, toCallSteps := stmt.To.ModuleCallTraversals() 51 52 modCfg := rootCfg.Descendent(stmtMod) 53 if pkgAddr := callsThroughModulePackage(modCfg, fromCallSteps); pkgAddr != nil { 54 diags = diags.Append(&hcl.Diagnostic{ 55 Severity: hcl.DiagError, 56 Summary: "Cross-package move statement", 57 Detail: fmt.Sprintf( 58 "This statement declares a move from an object declared in external module package %q. Move statements can be only within a single module package.", 59 pkgAddr, 60 ), 61 Subject: stmt.DeclRange.ToHCL().Ptr(), 62 }) 63 } 64 if pkgAddr := callsThroughModulePackage(modCfg, toCallSteps); pkgAddr != nil { 65 diags = diags.Append(&hcl.Diagnostic{ 66 Severity: hcl.DiagError, 67 Summary: "Cross-package move statement", 68 Detail: fmt.Sprintf( 69 "This statement declares a move to an object declared in external module package %q. Move statements can be only within a single module package.", 70 pkgAddr, 71 ), 72 Subject: stmt.DeclRange.ToHCL().Ptr(), 73 }) 74 } 75 76 for _, modInst := range declaredInsts.InstancesForModule(stmtMod) { 77 78 absFrom := stmt.From.InModuleInstance(modInst) 79 absTo := stmt.To.InModuleInstance(modInst) 80 fromKey := absFrom.UniqueKey() 81 toKey := absTo.UniqueKey() 82 83 if fromKey == toKey { 84 diags = diags.Append(&hcl.Diagnostic{ 85 Severity: hcl.DiagError, 86 Summary: "Redundant move statement", 87 Detail: fmt.Sprintf( 88 "This statement declares a move from %s to the same address, which is the same as not declaring this move at all.", 89 absFrom, 90 ), 91 Subject: stmt.DeclRange.ToHCL().Ptr(), 92 }) 93 continue 94 } 95 96 var noun string 97 var shortNoun string 98 switch absFrom.(type) { 99 case addrs.ModuleInstance: 100 noun = "module instance" 101 shortNoun = "instance" 102 case addrs.AbsModuleCall: 103 noun = "module call" 104 shortNoun = "call" 105 case addrs.AbsResourceInstance: 106 noun = "resource instance" 107 shortNoun = "instance" 108 case addrs.AbsResource: 109 noun = "resource" 110 shortNoun = "resource" 111 default: 112 // The above cases should cover all of the AbsMoveable types 113 panic("unsupported AbsMovable address type") 114 } 115 116 // It's invalid to have a move statement whose "from" address 117 // refers to something that is still declared in the configuration. 118 if moveableObjectExists(absFrom, declaredInsts) { 119 conflictRange, hasRange := movableObjectDeclRange(absFrom, rootCfg) 120 declaredAt := "" 121 if hasRange { 122 // NOTE: It'd be pretty weird to _not_ have a range, since 123 // we're only in this codepath because the plan phase 124 // thought this object existed in the configuration. 125 declaredAt = fmt.Sprintf(" at %s", conflictRange.StartString()) 126 } 127 128 diags = diags.Append(&hcl.Diagnostic{ 129 Severity: hcl.DiagError, 130 Summary: "Moved object still exists", 131 Detail: fmt.Sprintf( 132 "This statement declares a move from %s, but that %s is still declared%s.\n\nChange your configuration so that this %s will be declared as %s instead.", 133 absFrom, noun, declaredAt, shortNoun, absTo, 134 ), 135 Subject: stmt.DeclRange.ToHCL().Ptr(), 136 }) 137 } 138 139 // There can only be one destination for each source address. 140 if existing, exists := stmtFrom[fromKey]; exists { 141 if existing.Other.UniqueKey() != toKey { 142 diags = diags.Append(&hcl.Diagnostic{ 143 Severity: hcl.DiagError, 144 Summary: "Ambiguous move statements", 145 Detail: fmt.Sprintf( 146 "A statement at %s declared that %s moved to %s, but this statement instead declares that it moved to %s.\n\nEach %s can move to only one destination %s.", 147 existing.StmtRange.StartString(), absFrom, existing.Other, absTo, 148 noun, shortNoun, 149 ), 150 Subject: stmt.DeclRange.ToHCL().Ptr(), 151 }) 152 } 153 } else { 154 stmtFrom[fromKey] = AbsMoveEndpoint{ 155 Other: absTo, 156 StmtRange: stmt.DeclRange, 157 } 158 } 159 160 // There can only be one source for each destination address. 161 if existing, exists := stmtTo[toKey]; exists { 162 if existing.Other.UniqueKey() != fromKey { 163 diags = diags.Append(&hcl.Diagnostic{ 164 Severity: hcl.DiagError, 165 Summary: "Ambiguous move statements", 166 Detail: fmt.Sprintf( 167 "A statement at %s declared that %s moved to %s, but this statement instead declares that %s moved there.\n\nEach %s can have moved from only one source %s.", 168 existing.StmtRange.StartString(), existing.Other, absTo, absFrom, 169 noun, shortNoun, 170 ), 171 Subject: stmt.DeclRange.ToHCL().Ptr(), 172 }) 173 } 174 } else { 175 stmtTo[toKey] = AbsMoveEndpoint{ 176 Other: absFrom, 177 StmtRange: stmt.DeclRange, 178 } 179 } 180 181 } 182 } 183 184 // If we're not already returning other errors then we'll also check for 185 // and report cycles. 186 // 187 // Cycles alone are difficult to report in a helpful way because we don't 188 // have enough context to guess the user's intent. However, some particular 189 // mistakes that might lead to a cycle can also be caught by other 190 // validation rules above where we can make better suggestions, and so 191 // we'll use a cycle report only as a last resort. 192 if !diags.HasErrors() { 193 for _, cycle := range g.Cycles() { 194 // Reporting cycles is awkward because there isn't any definitive 195 // way to decide which of the objects in the cycle is the cause of 196 // the problem. Therefore we'll just list them all out and leave 197 // the user to figure it out. :( 198 stmtStrs := make([]string, 0, len(cycle)) 199 for _, stmtI := range cycle { 200 // move statement graph nodes are pointers to move statements 201 stmt := stmtI.(*MoveStatement) 202 stmtStrs = append(stmtStrs, fmt.Sprintf( 203 "\n - %s: %s → %s", 204 stmt.DeclRange.StartString(), 205 stmt.From.String(), 206 stmt.To.String(), 207 )) 208 } 209 sort.Strings(stmtStrs) // just to make the order deterministic 210 211 diags = diags.Append(tfdiags.Sourceless( 212 tfdiags.Error, 213 "Cyclic dependency in move statements", 214 fmt.Sprintf( 215 "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.", 216 strings.Join(stmtStrs, ""), 217 ), 218 )) 219 } 220 } 221 222 return diags 223 } 224 225 func moveableObjectExists(addr addrs.AbsMoveable, in instances.Set) bool { 226 switch addr := addr.(type) { 227 case addrs.ModuleInstance: 228 return in.HasModuleInstance(addr) 229 case addrs.AbsModuleCall: 230 return in.HasModuleCall(addr) 231 case addrs.AbsResourceInstance: 232 return in.HasResourceInstance(addr) 233 case addrs.AbsResource: 234 return in.HasResource(addr) 235 default: 236 // The above cases should cover all of the AbsMoveable types 237 panic("unsupported AbsMovable address type") 238 } 239 } 240 241 func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiags.SourceRange, bool) { 242 switch addr := addr.(type) { 243 case addrs.ModuleInstance: 244 // For a module instance we're actually looking for the call that 245 // declared it, which belongs to the parent module. 246 // (NOTE: This assumes "addr" can never be the root module instance, 247 // because the root module is never moveable.) 248 parentAddr, callAddr := addr.Call() 249 modCfg := cfg.DescendentForInstance(parentAddr) 250 if modCfg == nil { 251 return tfdiags.SourceRange{}, false 252 } 253 call := modCfg.Module.ModuleCalls[callAddr.Name] 254 if call == nil { 255 return tfdiags.SourceRange{}, false 256 } 257 258 // If the call has either count or for_each set then we'll "blame" 259 // that expression, rather than the block as a whole, because it's 260 // the expression that decides which instances are available. 261 switch { 262 case call.ForEach != nil: 263 return tfdiags.SourceRangeFromHCL(call.ForEach.Range()), true 264 case call.Count != nil: 265 return tfdiags.SourceRangeFromHCL(call.Count.Range()), true 266 default: 267 return tfdiags.SourceRangeFromHCL(call.DeclRange), true 268 } 269 case addrs.AbsModuleCall: 270 modCfg := cfg.DescendentForInstance(addr.Module) 271 if modCfg == nil { 272 return tfdiags.SourceRange{}, false 273 } 274 call := modCfg.Module.ModuleCalls[addr.Call.Name] 275 if call == nil { 276 return tfdiags.SourceRange{}, false 277 } 278 return tfdiags.SourceRangeFromHCL(call.DeclRange), true 279 case addrs.AbsResourceInstance: 280 modCfg := cfg.DescendentForInstance(addr.Module) 281 if modCfg == nil { 282 return tfdiags.SourceRange{}, false 283 } 284 rc := modCfg.Module.ResourceByAddr(addr.Resource.Resource) 285 if rc == nil { 286 return tfdiags.SourceRange{}, false 287 } 288 289 // If the resource has either count or for_each set then we'll "blame" 290 // that expression, rather than the block as a whole, because it's 291 // the expression that decides which instances are available. 292 switch { 293 case rc.ForEach != nil: 294 return tfdiags.SourceRangeFromHCL(rc.ForEach.Range()), true 295 case rc.Count != nil: 296 return tfdiags.SourceRangeFromHCL(rc.Count.Range()), true 297 default: 298 return tfdiags.SourceRangeFromHCL(rc.DeclRange), true 299 } 300 case addrs.AbsResource: 301 modCfg := cfg.DescendentForInstance(addr.Module) 302 if modCfg == nil { 303 return tfdiags.SourceRange{}, false 304 } 305 rc := modCfg.Module.ResourceByAddr(addr.Resource) 306 if rc == nil { 307 return tfdiags.SourceRange{}, false 308 } 309 return tfdiags.SourceRangeFromHCL(rc.DeclRange), true 310 default: 311 // The above cases should cover all of the AbsMoveable types 312 panic("unsupported AbsMovable address type") 313 } 314 } 315 316 func callsThroughModulePackage(modCfg *configs.Config, callSteps []addrs.ModuleCall) addrs.ModuleSource { 317 var sourceAddr addrs.ModuleSource 318 current := modCfg 319 for _, step := range callSteps { 320 call := current.Module.ModuleCalls[step.Name] 321 if call == nil { 322 break 323 } 324 if call.EntersNewPackage() { 325 sourceAddr = call.SourceAddr 326 } 327 current = modCfg.Children[step.Name] 328 if current == nil { 329 // Weird to have a call but not a config, but we'll tolerate 330 // it to avoid crashing here. 331 break 332 } 333 } 334 return sourceAddr 335 }