github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/move_endpoint.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package addrs 5 6 import ( 7 "fmt" 8 9 "github.com/hashicorp/hcl/v2" 10 "github.com/terramate-io/tf/tfdiags" 11 ) 12 13 // MoveEndpoint is to AbsMoveable and ConfigMoveable what Target is to 14 // Targetable: a wrapping struct that captures the result of decoding an HCL 15 // traversal representing a relative path from the current module to 16 // a moveable object. 17 // 18 // Its name reflects that its primary purpose is for the "from" and "to" 19 // addresses in a "moved" statement in the configuration, but it's also 20 // valid to use MoveEndpoint for other similar mechanisms that give 21 // Terraform hints about historical configuration changes that might 22 // prompt creating a different plan than Terraform would by default. 23 // 24 // To obtain a full address from a MoveEndpoint you must use 25 // either the package function UnifyMoveEndpoints (to get an AbsMoveable) or 26 // the method ConfigMoveable (to get a ConfigMoveable). 27 type MoveEndpoint struct { 28 // SourceRange is the location of the physical endpoint address 29 // in configuration, if this MoveEndpoint was decoded from a 30 // configuration expresson. 31 SourceRange tfdiags.SourceRange 32 33 // Internally we (ab)use AbsMoveable as the representation of our 34 // relative address, even though everywhere else in Terraform 35 // AbsMoveable always represents a fully-absolute address. 36 // In practice, due to the implementation of ParseMoveEndpoint, 37 // this is always either a ModuleInstance or an AbsResourceInstance, 38 // and we only consider the possibility of interpreting it as 39 // a AbsModuleCall or an AbsResource in UnifyMoveEndpoints. 40 // This is intentionally unexported to encapsulate this unusual 41 // meaning of AbsMoveable. 42 relSubject AbsMoveable 43 } 44 45 func (e *MoveEndpoint) ObjectKind() MoveEndpointKind { 46 return absMoveableEndpointKind(e.relSubject) 47 } 48 49 func (e *MoveEndpoint) String() string { 50 // Our internal pseudo-AbsMoveable representing the relative 51 // address (either ModuleInstance or AbsResourceInstance) is 52 // a good enough proxy for the relative move endpoint address 53 // serialization. 54 return e.relSubject.String() 55 } 56 57 func (e *MoveEndpoint) Equal(other *MoveEndpoint) bool { 58 switch { 59 case (e == nil) != (other == nil): 60 return false 61 case e == nil: 62 return true 63 default: 64 // Since we only use ModuleInstance and AbsResourceInstance in our 65 // string representation, we have no ambiguity between address types 66 // and can safely just compare the string representations to 67 // compare the relSubject values. 68 return e.String() == other.String() && e.SourceRange == other.SourceRange 69 } 70 } 71 72 // MightUnifyWith returns true if it is possible that a later call to 73 // UnifyMoveEndpoints might succeed if given the reciever and the other 74 // given endpoint. 75 // 76 // This is intended for early static validation of obviously-wrong situations, 77 // although there are still various semantic errors that this cannot catch. 78 func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool { 79 // For our purposes here we'll just do a unify without a base module 80 // address, because the rules for whether unify can succeed depend 81 // only on the relative part of the addresses, not on which module 82 // they were declared in. 83 from, to := UnifyMoveEndpoints(RootModule, e, other) 84 return from != nil && to != nil 85 } 86 87 // ConfigMovable transforms the reciever into a ConfigMovable by resolving it 88 // relative to the given base module, which should be the module where 89 // the MoveEndpoint expression was found. 90 // 91 // The result is useful for finding the target object in the configuration, 92 // but it's not sufficient for fully interpreting a move statement because 93 // it lacks the specific module and resource instance keys. 94 func (e *MoveEndpoint) ConfigMoveable(baseModule Module) ConfigMoveable { 95 addr := e.relSubject 96 switch addr := addr.(type) { 97 case ModuleInstance: 98 ret := make(Module, 0, len(baseModule)+len(addr)) 99 ret = append(ret, baseModule...) 100 ret = append(ret, addr.Module()...) 101 return ret 102 case AbsResourceInstance: 103 moduleAddr := make(Module, 0, len(baseModule)+len(addr.Module)) 104 moduleAddr = append(moduleAddr, baseModule...) 105 moduleAddr = append(moduleAddr, addr.Module.Module()...) 106 return ConfigResource{ 107 Module: moduleAddr, 108 Resource: addr.Resource.Resource, 109 } 110 default: 111 // The above should be exhaustive for all of the types 112 // that ParseMoveEndpoint produces as our intermediate 113 // address representation. 114 panic(fmt.Sprintf("unsupported address type %T", addr)) 115 } 116 117 } 118 119 // ParseMoveEndpoint attempts to interpret the given traversal as a 120 // "move endpoint" address, which is a relative path from the module containing 121 // the traversal to a movable object in either the same module or in some 122 // child module. 123 // 124 // This deals only with the syntactic element of a move endpoint expression 125 // in configuration. Before the result will be useful you'll need to combine 126 // it with the address of the module where it was declared in order to get 127 // an absolute address relative to the root module. 128 func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) { 129 path, remain, diags := parseModuleInstancePrefix(traversal) 130 if diags.HasErrors() { 131 return nil, diags 132 } 133 134 rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange()) 135 136 if len(remain) == 0 { 137 return &MoveEndpoint{ 138 relSubject: path, 139 SourceRange: rng, 140 }, diags 141 } 142 143 riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain) 144 diags = diags.Append(moreDiags) 145 if diags.HasErrors() { 146 return nil, diags 147 } 148 149 return &MoveEndpoint{ 150 relSubject: riAddr, 151 SourceRange: rng, 152 }, diags 153 } 154 155 // UnifyMoveEndpoints takes a pair of MoveEndpoint objects representing the 156 // "from" and "to" addresses in a moved block, and returns a pair of 157 // MoveEndpointInModule addresses guaranteed to be of the same dynamic type 158 // that represent what the two MoveEndpoint addresses refer to. 159 // 160 // moduleAddr must be the address of the module where the move was declared. 161 // 162 // This function deals both with the conversion from relative to absolute 163 // addresses and with resolving the ambiguity between no-key instance 164 // addresses and whole-object addresses, returning the least specific 165 // address type possible. 166 // 167 // Not all combinations of addresses are unifyable: the two addresses must 168 // either both include resources or both just be modules. If the two 169 // given addresses are incompatible then UnifyMoveEndpoints returns (nil, nil), 170 // in which case the caller should typically report an error to the user 171 // stating the unification constraints. 172 func UnifyMoveEndpoints(moduleAddr Module, relFrom, relTo *MoveEndpoint) (modFrom, modTo *MoveEndpointInModule) { 173 174 // First we'll make a decision about which address type we're 175 // ultimately trying to unify to. For our internal purposes 176 // here we're going to borrow TargetableAddrType just as a 177 // convenient way to talk about our address types, even though 178 // targetable address types are not 100% aligned with moveable 179 // address types. 180 fromType := relFrom.internalAddrType() 181 toType := relTo.internalAddrType() 182 var wantType TargetableAddrType 183 184 // Our goal here is to choose the whole-resource or whole-module-call 185 // addresses if both agree on it, but to use specific instance addresses 186 // otherwise. This is a somewhat-arbitrary way to resolve syntactic 187 // ambiguity between the two situations which allows both for renaming 188 // whole resources and for switching from a single-instance object to 189 // a multi-instance object. 190 switch { 191 case fromType == AbsResourceInstanceAddrType || toType == AbsResourceInstanceAddrType: 192 wantType = AbsResourceInstanceAddrType 193 case fromType == AbsResourceAddrType || toType == AbsResourceAddrType: 194 wantType = AbsResourceAddrType 195 case fromType == ModuleInstanceAddrType || toType == ModuleInstanceAddrType: 196 wantType = ModuleInstanceAddrType 197 case fromType == ModuleAddrType || toType == ModuleAddrType: 198 // NOTE: We're fudging a little here and using 199 // ModuleAddrType to represent AbsModuleCall rather 200 // than Module. 201 wantType = ModuleAddrType 202 default: 203 panic("unhandled move address types") 204 } 205 206 modFrom = relFrom.prepareMoveEndpointInModule(moduleAddr, wantType) 207 modTo = relTo.prepareMoveEndpointInModule(moduleAddr, wantType) 208 if modFrom == nil || modTo == nil { 209 // if either of them failed then they both failed, to make the 210 // caller's life a little easier. 211 return nil, nil 212 } 213 return modFrom, modTo 214 } 215 216 func (e *MoveEndpoint) prepareMoveEndpointInModule(moduleAddr Module, wantType TargetableAddrType) *MoveEndpointInModule { 217 // relAddr can only be either AbsResourceInstance or ModuleInstance, the 218 // internal intermediate representation produced by ParseMoveEndpoint. 219 relAddr := e.relSubject 220 221 switch relAddr := relAddr.(type) { 222 case ModuleInstance: 223 switch wantType { 224 case ModuleInstanceAddrType: 225 // Since our internal representation is already a module instance, 226 // we can just rewrap this one. 227 return &MoveEndpointInModule{ 228 SourceRange: e.SourceRange, 229 module: moduleAddr, 230 relSubject: relAddr, 231 } 232 case ModuleAddrType: 233 // NOTE: We're fudging a little here and using 234 // ModuleAddrType to represent AbsModuleCall rather 235 // than Module. 236 callerAddr, callAddr := relAddr.Call() 237 absCallAddr := AbsModuleCall{ 238 Module: callerAddr, 239 Call: callAddr, 240 } 241 return &MoveEndpointInModule{ 242 SourceRange: e.SourceRange, 243 module: moduleAddr, 244 relSubject: absCallAddr, 245 } 246 default: 247 return nil // can't make any other types from a ModuleInstance 248 } 249 case AbsResourceInstance: 250 switch wantType { 251 case AbsResourceInstanceAddrType: 252 return &MoveEndpointInModule{ 253 SourceRange: e.SourceRange, 254 module: moduleAddr, 255 relSubject: relAddr, 256 } 257 case AbsResourceAddrType: 258 return &MoveEndpointInModule{ 259 SourceRange: e.SourceRange, 260 module: moduleAddr, 261 relSubject: relAddr.ContainingResource(), 262 } 263 default: 264 return nil // can't make any other types from an AbsResourceInstance 265 } 266 default: 267 panic(fmt.Sprintf("unhandled address type %T", relAddr)) 268 } 269 } 270 271 // internalAddrType helps facilitate our slight abuse of TargetableAddrType 272 // as a way to talk about our different possible result address types in 273 // UnifyMoveEndpoints. 274 // 275 // It's not really correct to use TargetableAddrType in this way, because 276 // it's for Targetable rather than for AbsMoveable, but as long as the two 277 // remain aligned enough it saves introducing yet another enumeration with 278 // similar members that would be for internal use only anyway. 279 func (e *MoveEndpoint) internalAddrType() TargetableAddrType { 280 switch addr := e.relSubject.(type) { 281 case ModuleInstance: 282 if !addr.IsRoot() && addr[len(addr)-1].InstanceKey == NoKey { 283 // NOTE: We're fudging a little here and using 284 // ModuleAddrType to represent AbsModuleCall rather 285 // than Module. 286 return ModuleAddrType 287 } 288 return ModuleInstanceAddrType 289 case AbsResourceInstance: 290 if addr.Resource.Key == NoKey { 291 return AbsResourceAddrType 292 } 293 return AbsResourceInstanceAddrType 294 default: 295 // The above should cover all of the address types produced 296 // by ParseMoveEndpoint. 297 panic(fmt.Sprintf("unsupported address type %T", addr)) 298 } 299 }