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