github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/parse_ref.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package addrs 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/terramate-io/tf/tfdiags" 15 ) 16 17 // Reference describes a reference to an address with source location 18 // information. 19 type Reference struct { 20 Subject Referenceable 21 SourceRange tfdiags.SourceRange 22 Remaining hcl.Traversal 23 } 24 25 // DisplayString returns a string that approximates the subject and remaining 26 // traversal of the reciever in a way that resembles the Terraform language 27 // syntax that could've produced it. 28 // 29 // It's not guaranteed to actually be a valid Terraform language expression, 30 // since the intended use here is primarily for UI messages such as 31 // diagnostics. 32 func (r *Reference) DisplayString() string { 33 if len(r.Remaining) == 0 { 34 // Easy case: we can just return the subject's string. 35 return r.Subject.String() 36 } 37 38 var ret strings.Builder 39 ret.WriteString(r.Subject.String()) 40 for _, step := range r.Remaining { 41 switch tStep := step.(type) { 42 case hcl.TraverseRoot: 43 ret.WriteString(tStep.Name) 44 case hcl.TraverseAttr: 45 ret.WriteByte('.') 46 ret.WriteString(tStep.Name) 47 case hcl.TraverseIndex: 48 ret.WriteByte('[') 49 switch tStep.Key.Type() { 50 case cty.String: 51 ret.WriteString(fmt.Sprintf("%q", tStep.Key.AsString())) 52 case cty.Number: 53 bf := tStep.Key.AsBigFloat() 54 ret.WriteString(bf.Text('g', 10)) 55 } 56 ret.WriteByte(']') 57 } 58 } 59 return ret.String() 60 } 61 62 // ParseRef attempts to extract a referencable address from the prefix of the 63 // given traversal, which must be an absolute traversal or this function 64 // will panic. 65 // 66 // If no error diagnostics are returned, the returned reference includes the 67 // address that was extracted, the source range it was extracted from, and any 68 // remaining relative traversal that was not consumed as part of the 69 // reference. 70 // 71 // If error diagnostics are returned then the Reference value is invalid and 72 // must not be used. 73 func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 74 ref, diags := parseRef(traversal) 75 76 // Normalize a little to make life easier for callers. 77 if ref != nil { 78 if len(ref.Remaining) == 0 { 79 ref.Remaining = nil 80 } 81 } 82 83 return ref, diags 84 } 85 86 // ParseRefFromTestingScope adds check blocks and outputs into the available 87 // references returned by ParseRef. 88 // 89 // The testing files and functionality have a slightly expanded referencing 90 // scope and so should use this function to retrieve references. 91 func ParseRefFromTestingScope(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 92 root := traversal.RootName() 93 94 var diags tfdiags.Diagnostics 95 var reference *Reference 96 97 switch root { 98 case "output": 99 name, rng, remain, outputDiags := parseSingleAttrRef(traversal) 100 reference = &Reference{ 101 Subject: OutputValue{Name: name}, 102 SourceRange: tfdiags.SourceRangeFromHCL(rng), 103 Remaining: remain, 104 } 105 diags = outputDiags 106 case "check": 107 name, rng, remain, checkDiags := parseSingleAttrRef(traversal) 108 reference = &Reference{ 109 Subject: Check{Name: name}, 110 SourceRange: tfdiags.SourceRangeFromHCL(rng), 111 Remaining: remain, 112 } 113 diags = checkDiags 114 } 115 116 if reference != nil { 117 if len(reference.Remaining) == 0 { 118 reference.Remaining = nil 119 } 120 return reference, diags 121 } 122 123 // If it's not an output or a check block, then just parse it as normal. 124 return ParseRef(traversal) 125 } 126 127 // ParseRefStr is a helper wrapper around ParseRef that takes a string 128 // and parses it with the HCL native syntax traversal parser before 129 // interpreting it. 130 // 131 // This should be used only in specialized situations since it will cause the 132 // created references to not have any meaningful source location information. 133 // If a reference string is coming from a source that should be identified in 134 // error messages then the caller should instead parse it directly using a 135 // suitable function from the HCL API and pass the traversal itself to 136 // ParseRef. 137 // 138 // Error diagnostics are returned if either the parsing fails or the analysis 139 // of the traversal fails. There is no way for the caller to distinguish the 140 // two kinds of diagnostics programmatically. If error diagnostics are returned 141 // the returned reference may be nil or incomplete. 142 func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) { 143 var diags tfdiags.Diagnostics 144 145 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 146 diags = diags.Append(parseDiags) 147 if parseDiags.HasErrors() { 148 return nil, diags 149 } 150 151 ref, targetDiags := ParseRef(traversal) 152 diags = diags.Append(targetDiags) 153 return ref, diags 154 } 155 156 // ParseRefStrFromTestingScope matches ParseRefStr except it supports the 157 // references supported by ParseRefFromTestingScope. 158 func ParseRefStrFromTestingScope(str string) (*Reference, tfdiags.Diagnostics) { 159 var diags tfdiags.Diagnostics 160 161 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 162 diags = diags.Append(parseDiags) 163 if parseDiags.HasErrors() { 164 return nil, diags 165 } 166 167 ref, targetDiags := ParseRefFromTestingScope(traversal) 168 diags = diags.Append(targetDiags) 169 return ref, diags 170 } 171 172 func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 173 var diags tfdiags.Diagnostics 174 175 root := traversal.RootName() 176 rootRange := traversal[0].SourceRange() 177 178 switch root { 179 180 case "count": 181 name, rng, remain, diags := parseSingleAttrRef(traversal) 182 return &Reference{ 183 Subject: CountAttr{Name: name}, 184 SourceRange: tfdiags.SourceRangeFromHCL(rng), 185 Remaining: remain, 186 }, diags 187 188 case "each": 189 name, rng, remain, diags := parseSingleAttrRef(traversal) 190 return &Reference{ 191 Subject: ForEachAttr{Name: name}, 192 SourceRange: tfdiags.SourceRangeFromHCL(rng), 193 Remaining: remain, 194 }, diags 195 196 case "data": 197 if len(traversal) < 3 { 198 diags = diags.Append(&hcl.Diagnostic{ 199 Severity: hcl.DiagError, 200 Summary: "Invalid reference", 201 Detail: `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 202 Subject: traversal.SourceRange().Ptr(), 203 }) 204 return nil, diags 205 } 206 remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser 207 return parseResourceRef(DataResourceMode, rootRange, remain) 208 209 case "resource": 210 // This is an alias for the normal case of just using a managed resource 211 // type as a top-level symbol, which will serve as an escape mechanism 212 // if a later edition of the Terraform language introduces a new 213 // reference prefix that conflicts with a resource type name in an 214 // existing provider. In that case, the edition upgrade tool can 215 // rewrite foo.bar into resource.foo.bar to ensure that "foo" remains 216 // interpreted as a resource type name rather than as the new reserved 217 // word. 218 if len(traversal) < 3 { 219 diags = diags.Append(&hcl.Diagnostic{ 220 Severity: hcl.DiagError, 221 Summary: "Invalid reference", 222 Detail: `The "resource" object must be followed by two attribute names: the resource type and the resource name.`, 223 Subject: traversal.SourceRange().Ptr(), 224 }) 225 return nil, diags 226 } 227 remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser 228 return parseResourceRef(ManagedResourceMode, rootRange, remain) 229 230 case "local": 231 name, rng, remain, diags := parseSingleAttrRef(traversal) 232 return &Reference{ 233 Subject: LocalValue{Name: name}, 234 SourceRange: tfdiags.SourceRangeFromHCL(rng), 235 Remaining: remain, 236 }, diags 237 238 case "module": 239 callName, callRange, remain, diags := parseSingleAttrRef(traversal) 240 if diags.HasErrors() { 241 return nil, diags 242 } 243 244 // A traversal starting with "module" can either be a reference to an 245 // entire module, or to a single output from a module instance, 246 // depending on what we find after this introducer. 247 callInstance := ModuleCallInstance{ 248 Call: ModuleCall{ 249 Name: callName, 250 }, 251 Key: NoKey, 252 } 253 254 if len(remain) == 0 { 255 // Reference to an entire module. Might alternatively be a 256 // reference to a single instance of a particular module, but the 257 // caller will need to deal with that ambiguity since we don't have 258 // enough context here. 259 return &Reference{ 260 Subject: callInstance.Call, 261 SourceRange: tfdiags.SourceRangeFromHCL(callRange), 262 Remaining: remain, 263 }, diags 264 } 265 266 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 267 var err error 268 callInstance.Key, err = ParseInstanceKey(idxTrav.Key) 269 if err != nil { 270 diags = diags.Append(&hcl.Diagnostic{ 271 Severity: hcl.DiagError, 272 Summary: "Invalid index key", 273 Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), 274 Subject: &idxTrav.SrcRange, 275 }) 276 return nil, diags 277 } 278 remain = remain[1:] 279 280 if len(remain) == 0 { 281 // Also a reference to an entire module instance, but we have a key 282 // now. 283 return &Reference{ 284 Subject: callInstance, 285 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)), 286 Remaining: remain, 287 }, diags 288 } 289 } 290 291 if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { 292 remain = remain[1:] 293 return &Reference{ 294 Subject: ModuleCallInstanceOutput{ 295 Name: attrTrav.Name, 296 Call: callInstance, 297 }, 298 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)), 299 Remaining: remain, 300 }, diags 301 } 302 303 diags = diags.Append(&hcl.Diagnostic{ 304 Severity: hcl.DiagError, 305 Summary: "Invalid reference", 306 Detail: "Module instance objects do not support this operation.", 307 Subject: remain[0].SourceRange().Ptr(), 308 }) 309 return nil, diags 310 311 case "path": 312 name, rng, remain, diags := parseSingleAttrRef(traversal) 313 return &Reference{ 314 Subject: PathAttr{Name: name}, 315 SourceRange: tfdiags.SourceRangeFromHCL(rng), 316 Remaining: remain, 317 }, diags 318 319 case "self": 320 return &Reference{ 321 Subject: Self, 322 SourceRange: tfdiags.SourceRangeFromHCL(rootRange), 323 Remaining: traversal[1:], 324 }, diags 325 326 case "terraform": 327 name, rng, remain, diags := parseSingleAttrRef(traversal) 328 return &Reference{ 329 Subject: TerraformAttr{Name: name}, 330 SourceRange: tfdiags.SourceRangeFromHCL(rng), 331 Remaining: remain, 332 }, diags 333 334 case "var": 335 name, rng, remain, diags := parseSingleAttrRef(traversal) 336 return &Reference{ 337 Subject: InputVariable{Name: name}, 338 SourceRange: tfdiags.SourceRangeFromHCL(rng), 339 Remaining: remain, 340 }, diags 341 342 case "template", "lazy", "arg": 343 // These names are all pre-emptively reserved in the hope of landing 344 // some version of "template values" or "lazy expressions" feature 345 // before the next opt-in language edition, but don't yet do anything. 346 diags = diags.Append(&hcl.Diagnostic{ 347 Severity: hcl.DiagError, 348 Summary: "Reserved symbol name", 349 Detail: fmt.Sprintf("The symbol name %q is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name.", root), 350 Subject: rootRange.Ptr(), 351 }) 352 return nil, diags 353 354 default: 355 return parseResourceRef(ManagedResourceMode, rootRange, traversal) 356 } 357 } 358 359 func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 360 var diags tfdiags.Diagnostics 361 362 if len(traversal) < 2 { 363 diags = diags.Append(&hcl.Diagnostic{ 364 Severity: hcl.DiagError, 365 Summary: "Invalid reference", 366 Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 367 Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), 368 }) 369 return nil, diags 370 } 371 372 var typeName, name string 373 switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode 374 case hcl.TraverseRoot: 375 typeName = tt.Name 376 case hcl.TraverseAttr: 377 typeName = tt.Name 378 default: 379 // If it isn't a TraverseRoot then it must be a "data" reference. 380 diags = diags.Append(&hcl.Diagnostic{ 381 Severity: hcl.DiagError, 382 Summary: "Invalid reference", 383 Detail: `The "data" object does not support this operation.`, 384 Subject: traversal[0].SourceRange().Ptr(), 385 }) 386 return nil, diags 387 } 388 389 attrTrav, ok := traversal[1].(hcl.TraverseAttr) 390 if !ok { 391 var what string 392 switch mode { 393 case DataResourceMode: 394 what = "data source" 395 default: 396 what = "resource type" 397 } 398 diags = diags.Append(&hcl.Diagnostic{ 399 Severity: hcl.DiagError, 400 Summary: "Invalid reference", 401 Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), 402 Subject: traversal[1].SourceRange().Ptr(), 403 }) 404 return nil, diags 405 } 406 name = attrTrav.Name 407 rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) 408 remain := traversal[2:] 409 410 resourceAddr := Resource{ 411 Mode: mode, 412 Type: typeName, 413 Name: name, 414 } 415 resourceInstAddr := ResourceInstance{ 416 Resource: resourceAddr, 417 Key: NoKey, 418 } 419 420 if len(remain) == 0 { 421 // This might actually be a reference to the collection of all instances 422 // of the resource, but we don't have enough context here to decide 423 // so we'll let the caller resolve that ambiguity. 424 return &Reference{ 425 Subject: resourceAddr, 426 SourceRange: tfdiags.SourceRangeFromHCL(rng), 427 }, diags 428 } 429 430 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 431 var err error 432 resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) 433 if err != nil { 434 diags = diags.Append(&hcl.Diagnostic{ 435 Severity: hcl.DiagError, 436 Summary: "Invalid index key", 437 Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), 438 Subject: &idxTrav.SrcRange, 439 }) 440 return nil, diags 441 } 442 remain = remain[1:] 443 rng = hcl.RangeBetween(rng, idxTrav.SrcRange) 444 } 445 446 return &Reference{ 447 Subject: resourceInstAddr, 448 SourceRange: tfdiags.SourceRangeFromHCL(rng), 449 Remaining: remain, 450 }, diags 451 } 452 453 func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { 454 var diags tfdiags.Diagnostics 455 456 root := traversal.RootName() 457 rootRange := traversal[0].SourceRange() 458 459 if len(traversal) < 2 { 460 diags = diags.Append(&hcl.Diagnostic{ 461 Severity: hcl.DiagError, 462 Summary: "Invalid reference", 463 Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), 464 Subject: &rootRange, 465 }) 466 return "", hcl.Range{}, nil, diags 467 } 468 if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { 469 return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags 470 } 471 diags = diags.Append(&hcl.Diagnostic{ 472 Severity: hcl.DiagError, 473 Summary: "Invalid reference", 474 Detail: fmt.Sprintf("The %q object does not support this operation.", root), 475 Subject: traversal[1].SourceRange().Ptr(), 476 }) 477 return "", hcl.Range{}, nil, diags 478 }