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