github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/addrs/parse_target.go (about) 1 package addrs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2/hclsyntax" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/cycloidio/terraform/tfdiags" 10 ) 11 12 // Target describes a targeted address with source location information. 13 type Target struct { 14 Subject Targetable 15 SourceRange tfdiags.SourceRange 16 } 17 18 // ParseTarget attempts to interpret the given traversal as a targetable 19 // address. The given traversal must be absolute, or this function will 20 // panic. 21 // 22 // If no error diagnostics are returned, the returned target includes the 23 // address that was extracted and the source range it was extracted from. 24 // 25 // If error diagnostics are returned then the Target value is invalid and 26 // must not be used. 27 func ParseTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) { 28 path, remain, diags := parseModuleInstancePrefix(traversal) 29 if diags.HasErrors() { 30 return nil, diags 31 } 32 33 rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange()) 34 35 if len(remain) == 0 { 36 return &Target{ 37 Subject: path, 38 SourceRange: rng, 39 }, diags 40 } 41 42 riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain) 43 diags = diags.Append(moreDiags) 44 if diags.HasErrors() { 45 return nil, diags 46 } 47 48 var subject Targetable 49 switch { 50 case riAddr.Resource.Key == NoKey: 51 // We always assume that a no-key instance is meant to 52 // be referring to the whole resource, because the distinction 53 // doesn't really matter for targets anyway. 54 subject = riAddr.ContainingResource() 55 default: 56 subject = riAddr 57 } 58 59 return &Target{ 60 Subject: subject, 61 SourceRange: rng, 62 }, diags 63 } 64 65 func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { 66 // Note that this helper is used as part of both ParseTarget and 67 // ParseMoveEndpoint, so its error messages should be generic 68 // enough to suit both situations. 69 70 var diags tfdiags.Diagnostics 71 72 mode := ManagedResourceMode 73 if remain.RootName() == "data" { 74 mode = DataResourceMode 75 remain = remain[1:] 76 } 77 78 if len(remain) < 2 { 79 diags = diags.Append(&hcl.Diagnostic{ 80 Severity: hcl.DiagError, 81 Summary: "Invalid address", 82 Detail: "Resource specification must include a resource type and name.", 83 Subject: remain.SourceRange().Ptr(), 84 }) 85 return AbsResourceInstance{}, diags 86 } 87 88 var typeName, name string 89 switch tt := remain[0].(type) { 90 case hcl.TraverseRoot: 91 typeName = tt.Name 92 case hcl.TraverseAttr: 93 typeName = tt.Name 94 default: 95 switch mode { 96 case ManagedResourceMode: 97 diags = diags.Append(&hcl.Diagnostic{ 98 Severity: hcl.DiagError, 99 Summary: "Invalid address", 100 Detail: "A resource type name is required.", 101 Subject: remain[0].SourceRange().Ptr(), 102 }) 103 case DataResourceMode: 104 diags = diags.Append(&hcl.Diagnostic{ 105 Severity: hcl.DiagError, 106 Summary: "Invalid address", 107 Detail: "A data source name is required.", 108 Subject: remain[0].SourceRange().Ptr(), 109 }) 110 default: 111 panic("unknown mode") 112 } 113 return AbsResourceInstance{}, diags 114 } 115 116 switch tt := remain[1].(type) { 117 case hcl.TraverseAttr: 118 name = tt.Name 119 default: 120 diags = diags.Append(&hcl.Diagnostic{ 121 Severity: hcl.DiagError, 122 Summary: "Invalid address", 123 Detail: "A resource name is required.", 124 Subject: remain[1].SourceRange().Ptr(), 125 }) 126 return AbsResourceInstance{}, diags 127 } 128 129 remain = remain[2:] 130 switch len(remain) { 131 case 0: 132 return moduleAddr.ResourceInstance(mode, typeName, name, NoKey), diags 133 case 1: 134 if tt, ok := remain[0].(hcl.TraverseIndex); ok { 135 key, err := ParseInstanceKey(tt.Key) 136 if err != nil { 137 diags = diags.Append(&hcl.Diagnostic{ 138 Severity: hcl.DiagError, 139 Summary: "Invalid address", 140 Detail: fmt.Sprintf("Invalid resource instance key: %s.", err), 141 Subject: remain[0].SourceRange().Ptr(), 142 }) 143 return AbsResourceInstance{}, diags 144 } 145 146 return moduleAddr.ResourceInstance(mode, typeName, name, key), diags 147 } else { 148 diags = diags.Append(&hcl.Diagnostic{ 149 Severity: hcl.DiagError, 150 Summary: "Invalid address", 151 Detail: "Resource instance key must be given in square brackets.", 152 Subject: remain[0].SourceRange().Ptr(), 153 }) 154 return AbsResourceInstance{}, diags 155 } 156 default: 157 diags = diags.Append(&hcl.Diagnostic{ 158 Severity: hcl.DiagError, 159 Summary: "Invalid address", 160 Detail: "Unexpected extra operators after address.", 161 Subject: remain[1].SourceRange().Ptr(), 162 }) 163 return AbsResourceInstance{}, diags 164 } 165 } 166 167 // ParseTargetStr is a helper wrapper around ParseTarget that takes a string 168 // and parses it with the HCL native syntax traversal parser before 169 // interpreting it. 170 // 171 // This should be used only in specialized situations since it will cause the 172 // created references to not have any meaningful source location information. 173 // If a target string is coming from a source that should be identified in 174 // error messages then the caller should instead parse it directly using a 175 // suitable function from the HCL API and pass the traversal itself to 176 // ParseTarget. 177 // 178 // Error diagnostics are returned if either the parsing fails or the analysis 179 // of the traversal fails. There is no way for the caller to distinguish the 180 // two kinds of diagnostics programmatically. If error diagnostics are returned 181 // the returned target may be nil or incomplete. 182 func ParseTargetStr(str string) (*Target, tfdiags.Diagnostics) { 183 var diags tfdiags.Diagnostics 184 185 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 186 diags = diags.Append(parseDiags) 187 if parseDiags.HasErrors() { 188 return nil, diags 189 } 190 191 target, targetDiags := ParseTarget(traversal) 192 diags = diags.Append(targetDiags) 193 return target, diags 194 } 195 196 // ParseAbsResource attempts to interpret the given traversal as an absolute 197 // resource address, using the same syntax as expected by ParseTarget. 198 // 199 // If no error diagnostics are returned, the returned target includes the 200 // address that was extracted and the source range it was extracted from. 201 // 202 // If error diagnostics are returned then the AbsResource value is invalid and 203 // must not be used. 204 func ParseAbsResource(traversal hcl.Traversal) (AbsResource, tfdiags.Diagnostics) { 205 addr, diags := ParseTarget(traversal) 206 if diags.HasErrors() { 207 return AbsResource{}, diags 208 } 209 210 switch tt := addr.Subject.(type) { 211 212 case AbsResource: 213 return tt, diags 214 215 case AbsResourceInstance: // Catch likely user error with specialized message 216 // Assume that the last element of the traversal must be the index, 217 // since that's required for a valid resource instance address. 218 indexStep := traversal[len(traversal)-1] 219 diags = diags.Append(&hcl.Diagnostic{ 220 Severity: hcl.DiagError, 221 Summary: "Invalid address", 222 Detail: "A resource address is required. This instance key identifies a specific resource instance, which is not expected here.", 223 Subject: indexStep.SourceRange().Ptr(), 224 }) 225 return AbsResource{}, diags 226 227 case ModuleInstance: // Catch likely user error with specialized message 228 diags = diags.Append(&hcl.Diagnostic{ 229 Severity: hcl.DiagError, 230 Summary: "Invalid address", 231 Detail: "A resource address is required here. The module path must be followed by a resource specification.", 232 Subject: traversal.SourceRange().Ptr(), 233 }) 234 return AbsResource{}, diags 235 236 default: // Generic message for other address types 237 diags = diags.Append(&hcl.Diagnostic{ 238 Severity: hcl.DiagError, 239 Summary: "Invalid address", 240 Detail: "A resource address is required here.", 241 Subject: traversal.SourceRange().Ptr(), 242 }) 243 return AbsResource{}, diags 244 245 } 246 } 247 248 // ParseAbsResourceStr is a helper wrapper around ParseAbsResource that takes a 249 // string and parses it with the HCL native syntax traversal parser before 250 // interpreting it. 251 // 252 // Error diagnostics are returned if either the parsing fails or the analysis 253 // of the traversal fails. There is no way for the caller to distinguish the 254 // two kinds of diagnostics programmatically. If error diagnostics are returned 255 // the returned address may be incomplete. 256 // 257 // Since this function has no context about the source of the given string, 258 // any returned diagnostics will not have meaningful source location 259 // information. 260 func ParseAbsResourceStr(str string) (AbsResource, tfdiags.Diagnostics) { 261 var diags tfdiags.Diagnostics 262 263 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 264 diags = diags.Append(parseDiags) 265 if parseDiags.HasErrors() { 266 return AbsResource{}, diags 267 } 268 269 addr, addrDiags := ParseAbsResource(traversal) 270 diags = diags.Append(addrDiags) 271 return addr, diags 272 } 273 274 // ParseAbsResourceInstance attempts to interpret the given traversal as an 275 // absolute resource instance address, using the same syntax as expected by 276 // ParseTarget. 277 // 278 // If no error diagnostics are returned, the returned target includes the 279 // address that was extracted and the source range it was extracted from. 280 // 281 // If error diagnostics are returned then the AbsResource value is invalid and 282 // must not be used. 283 func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { 284 addr, diags := ParseTarget(traversal) 285 if diags.HasErrors() { 286 return AbsResourceInstance{}, diags 287 } 288 289 switch tt := addr.Subject.(type) { 290 291 case AbsResource: 292 return tt.Instance(NoKey), diags 293 294 case AbsResourceInstance: 295 return tt, diags 296 297 case ModuleInstance: // Catch likely user error with specialized message 298 diags = diags.Append(&hcl.Diagnostic{ 299 Severity: hcl.DiagError, 300 Summary: "Invalid address", 301 Detail: "A resource instance address is required here. The module path must be followed by a resource instance specification.", 302 Subject: traversal.SourceRange().Ptr(), 303 }) 304 return AbsResourceInstance{}, diags 305 306 default: // Generic message for other address types 307 diags = diags.Append(&hcl.Diagnostic{ 308 Severity: hcl.DiagError, 309 Summary: "Invalid address", 310 Detail: "A resource address is required here.", 311 Subject: traversal.SourceRange().Ptr(), 312 }) 313 return AbsResourceInstance{}, diags 314 315 } 316 } 317 318 // ParseAbsResourceInstanceStr is a helper wrapper around 319 // ParseAbsResourceInstance that takes a string and parses it with the HCL 320 // native syntax traversal parser before interpreting it. 321 // 322 // Error diagnostics are returned if either the parsing fails or the analysis 323 // of the traversal fails. There is no way for the caller to distinguish the 324 // two kinds of diagnostics programmatically. If error diagnostics are returned 325 // the returned address may be incomplete. 326 // 327 // Since this function has no context about the source of the given string, 328 // any returned diagnostics will not have meaningful source location 329 // information. 330 func ParseAbsResourceInstanceStr(str string) (AbsResourceInstance, tfdiags.Diagnostics) { 331 var diags tfdiags.Diagnostics 332 333 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 334 diags = diags.Append(parseDiags) 335 if parseDiags.HasErrors() { 336 return AbsResourceInstance{}, diags 337 } 338 339 addr, addrDiags := ParseAbsResourceInstance(traversal) 340 diags = diags.Append(addrDiags) 341 return addr, diags 342 } 343 344 // ModuleAddr returns the module address portion of the subject of 345 // the recieving target. 346 // 347 // Regardless of specific address type, all targets always include 348 // a module address. They might also include something in that 349 // module, which this method always discards if so. 350 func (t *Target) ModuleAddr() ModuleInstance { 351 switch addr := t.Subject.(type) { 352 case ModuleInstance: 353 return addr 354 case Module: 355 // We assume that a module address is really 356 // referring to a module path containing only 357 // single-instance modules. 358 return addr.UnkeyedInstanceShim() 359 case AbsResourceInstance: 360 return addr.Module 361 case AbsResource: 362 return addr.Module 363 default: 364 // The above cases should be exhaustive for all 365 // implementations of Targetable. 366 panic(fmt.Sprintf("unsupported target address type %T", addr)) 367 } 368 }