github.com/hugorut/terraform@v1.1.3/src/addrs/parse_ref.go (about) 1 package addrs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/hclsyntax" 8 "github.com/hugorut/terraform/src/tfdiags" 9 ) 10 11 // Reference describes a reference to an address with source location 12 // information. 13 type Reference struct { 14 Subject Referenceable 15 SourceRange tfdiags.SourceRange 16 Remaining hcl.Traversal 17 } 18 19 // ParseRef attempts to extract a referencable address from the prefix of the 20 // given traversal, which must be an absolute traversal or this function 21 // will panic. 22 // 23 // If no error diagnostics are returned, the returned reference includes the 24 // address that was extracted, the source range it was extracted from, and any 25 // remaining relative traversal that was not consumed as part of the 26 // reference. 27 // 28 // If error diagnostics are returned then the Reference value is invalid and 29 // must not be used. 30 func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 31 ref, diags := parseRef(traversal) 32 33 // Normalize a little to make life easier for callers. 34 if ref != nil { 35 if len(ref.Remaining) == 0 { 36 ref.Remaining = nil 37 } 38 } 39 40 return ref, diags 41 } 42 43 // ParseRefStr is a helper wrapper around ParseRef that takes a string 44 // and parses it with the HCL native syntax traversal parser before 45 // interpreting it. 46 // 47 // This should be used only in specialized situations since it will cause the 48 // created references to not have any meaningful source location information. 49 // If a reference string is coming from a source that should be identified in 50 // error messages then the caller should instead parse it directly using a 51 // suitable function from the HCL API and pass the traversal itself to 52 // ParseRef. 53 // 54 // Error diagnostics are returned if either the parsing fails or the analysis 55 // of the traversal fails. There is no way for the caller to distinguish the 56 // two kinds of diagnostics programmatically. If error diagnostics are returned 57 // the returned reference may be nil or incomplete. 58 func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) { 59 var diags tfdiags.Diagnostics 60 61 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 62 diags = diags.Append(parseDiags) 63 if parseDiags.HasErrors() { 64 return nil, diags 65 } 66 67 ref, targetDiags := ParseRef(traversal) 68 diags = diags.Append(targetDiags) 69 return ref, diags 70 } 71 72 func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 73 var diags tfdiags.Diagnostics 74 75 root := traversal.RootName() 76 rootRange := traversal[0].SourceRange() 77 78 switch root { 79 80 case "count": 81 name, rng, remain, diags := parseSingleAttrRef(traversal) 82 return &Reference{ 83 Subject: CountAttr{Name: name}, 84 SourceRange: tfdiags.SourceRangeFromHCL(rng), 85 Remaining: remain, 86 }, diags 87 88 case "each": 89 name, rng, remain, diags := parseSingleAttrRef(traversal) 90 return &Reference{ 91 Subject: ForEachAttr{Name: name}, 92 SourceRange: tfdiags.SourceRangeFromHCL(rng), 93 Remaining: remain, 94 }, diags 95 96 case "data": 97 if len(traversal) < 3 { 98 diags = diags.Append(&hcl.Diagnostic{ 99 Severity: hcl.DiagError, 100 Summary: "Invalid reference", 101 Detail: `The "data" object must be followed by two attribute names: the data source type and the resource name.`, 102 Subject: traversal.SourceRange().Ptr(), 103 }) 104 return nil, diags 105 } 106 remain := traversal[1:] // trim off "data" so we can use our shared resource reference parser 107 return parseResourceRef(DataResourceMode, rootRange, remain) 108 109 case "resource": 110 // This is an alias for the normal case of just using a managed resource 111 // type as a top-level symbol, which will serve as an escape mechanism 112 // if a later edition of the Terraform language introduces a new 113 // reference prefix that conflicts with a resource type name in an 114 // existing provider. In that case, the edition upgrade tool can 115 // rewrite foo.bar into resource.foo.bar to ensure that "foo" remains 116 // interpreted as a resource type name rather than as the new reserved 117 // word. 118 if len(traversal) < 3 { 119 diags = diags.Append(&hcl.Diagnostic{ 120 Severity: hcl.DiagError, 121 Summary: "Invalid reference", 122 Detail: `The "resource" object must be followed by two attribute names: the resource type and the resource name.`, 123 Subject: traversal.SourceRange().Ptr(), 124 }) 125 return nil, diags 126 } 127 remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser 128 return parseResourceRef(ManagedResourceMode, rootRange, remain) 129 130 case "local": 131 name, rng, remain, diags := parseSingleAttrRef(traversal) 132 return &Reference{ 133 Subject: LocalValue{Name: name}, 134 SourceRange: tfdiags.SourceRangeFromHCL(rng), 135 Remaining: remain, 136 }, diags 137 138 case "module": 139 callName, callRange, remain, diags := parseSingleAttrRef(traversal) 140 if diags.HasErrors() { 141 return nil, diags 142 } 143 144 // A traversal starting with "module" can either be a reference to an 145 // entire module, or to a single output from a module instance, 146 // depending on what we find after this introducer. 147 callInstance := ModuleCallInstance{ 148 Call: ModuleCall{ 149 Name: callName, 150 }, 151 Key: NoKey, 152 } 153 154 if len(remain) == 0 { 155 // Reference to an entire module. Might alternatively be a 156 // reference to a single instance of a particular module, but the 157 // caller will need to deal with that ambiguity since we don't have 158 // enough context here. 159 return &Reference{ 160 Subject: callInstance.Call, 161 SourceRange: tfdiags.SourceRangeFromHCL(callRange), 162 Remaining: remain, 163 }, diags 164 } 165 166 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 167 var err error 168 callInstance.Key, err = ParseInstanceKey(idxTrav.Key) 169 if err != nil { 170 diags = diags.Append(&hcl.Diagnostic{ 171 Severity: hcl.DiagError, 172 Summary: "Invalid index key", 173 Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), 174 Subject: &idxTrav.SrcRange, 175 }) 176 return nil, diags 177 } 178 remain = remain[1:] 179 180 if len(remain) == 0 { 181 // Also a reference to an entire module instance, but we have a key 182 // now. 183 return &Reference{ 184 Subject: callInstance, 185 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)), 186 Remaining: remain, 187 }, diags 188 } 189 } 190 191 if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { 192 remain = remain[1:] 193 return &Reference{ 194 Subject: ModuleCallInstanceOutput{ 195 Name: attrTrav.Name, 196 Call: callInstance, 197 }, 198 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)), 199 Remaining: remain, 200 }, diags 201 } 202 203 diags = diags.Append(&hcl.Diagnostic{ 204 Severity: hcl.DiagError, 205 Summary: "Invalid reference", 206 Detail: "Module instance objects do not support this operation.", 207 Subject: remain[0].SourceRange().Ptr(), 208 }) 209 return nil, diags 210 211 case "path": 212 name, rng, remain, diags := parseSingleAttrRef(traversal) 213 return &Reference{ 214 Subject: PathAttr{Name: name}, 215 SourceRange: tfdiags.SourceRangeFromHCL(rng), 216 Remaining: remain, 217 }, diags 218 219 case "self": 220 return &Reference{ 221 Subject: Self, 222 SourceRange: tfdiags.SourceRangeFromHCL(rootRange), 223 Remaining: traversal[1:], 224 }, diags 225 226 case "terraform": 227 name, rng, remain, diags := parseSingleAttrRef(traversal) 228 return &Reference{ 229 Subject: TerraformAttr{Name: name}, 230 SourceRange: tfdiags.SourceRangeFromHCL(rng), 231 Remaining: remain, 232 }, diags 233 234 case "var": 235 name, rng, remain, diags := parseSingleAttrRef(traversal) 236 return &Reference{ 237 Subject: InputVariable{Name: name}, 238 SourceRange: tfdiags.SourceRangeFromHCL(rng), 239 Remaining: remain, 240 }, diags 241 242 case "template", "lazy", "arg": 243 // These names are all pre-emptively reserved in the hope of landing 244 // some version of "template values" or "lazy expressions" feature 245 // before the next opt-in language edition, but don't yet do anything. 246 diags = diags.Append(&hcl.Diagnostic{ 247 Severity: hcl.DiagError, 248 Summary: "Reserved symbol name", 249 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), 250 Subject: rootRange.Ptr(), 251 }) 252 return nil, diags 253 254 default: 255 return parseResourceRef(ManagedResourceMode, rootRange, traversal) 256 } 257 } 258 259 func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 260 var diags tfdiags.Diagnostics 261 262 if len(traversal) < 2 { 263 diags = diags.Append(&hcl.Diagnostic{ 264 Severity: hcl.DiagError, 265 Summary: "Invalid reference", 266 Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 267 Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), 268 }) 269 return nil, diags 270 } 271 272 var typeName, name string 273 switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode 274 case hcl.TraverseRoot: 275 typeName = tt.Name 276 case hcl.TraverseAttr: 277 typeName = tt.Name 278 default: 279 // If it isn't a TraverseRoot then it must be a "data" reference. 280 diags = diags.Append(&hcl.Diagnostic{ 281 Severity: hcl.DiagError, 282 Summary: "Invalid reference", 283 Detail: `The "data" object does not support this operation.`, 284 Subject: traversal[0].SourceRange().Ptr(), 285 }) 286 return nil, diags 287 } 288 289 attrTrav, ok := traversal[1].(hcl.TraverseAttr) 290 if !ok { 291 var what string 292 switch mode { 293 case DataResourceMode: 294 what = "data source" 295 default: 296 what = "resource type" 297 } 298 diags = diags.Append(&hcl.Diagnostic{ 299 Severity: hcl.DiagError, 300 Summary: "Invalid reference", 301 Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), 302 Subject: traversal[1].SourceRange().Ptr(), 303 }) 304 return nil, diags 305 } 306 name = attrTrav.Name 307 rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) 308 remain := traversal[2:] 309 310 resourceAddr := Resource{ 311 Mode: mode, 312 Type: typeName, 313 Name: name, 314 } 315 resourceInstAddr := ResourceInstance{ 316 Resource: resourceAddr, 317 Key: NoKey, 318 } 319 320 if len(remain) == 0 { 321 // This might actually be a reference to the collection of all instances 322 // of the resource, but we don't have enough context here to decide 323 // so we'll let the caller resolve that ambiguity. 324 return &Reference{ 325 Subject: resourceAddr, 326 SourceRange: tfdiags.SourceRangeFromHCL(rng), 327 }, diags 328 } 329 330 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 331 var err error 332 resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) 333 if err != nil { 334 diags = diags.Append(&hcl.Diagnostic{ 335 Severity: hcl.DiagError, 336 Summary: "Invalid index key", 337 Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), 338 Subject: &idxTrav.SrcRange, 339 }) 340 return nil, diags 341 } 342 remain = remain[1:] 343 rng = hcl.RangeBetween(rng, idxTrav.SrcRange) 344 } 345 346 return &Reference{ 347 Subject: resourceInstAddr, 348 SourceRange: tfdiags.SourceRangeFromHCL(rng), 349 Remaining: remain, 350 }, diags 351 } 352 353 func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { 354 var diags tfdiags.Diagnostics 355 356 root := traversal.RootName() 357 rootRange := traversal[0].SourceRange() 358 359 if len(traversal) < 2 { 360 diags = diags.Append(&hcl.Diagnostic{ 361 Severity: hcl.DiagError, 362 Summary: "Invalid reference", 363 Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), 364 Subject: &rootRange, 365 }) 366 return "", hcl.Range{}, nil, diags 367 } 368 if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { 369 return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags 370 } 371 diags = diags.Append(&hcl.Diagnostic{ 372 Severity: hcl.DiagError, 373 Summary: "Invalid reference", 374 Detail: fmt.Sprintf("The %q object does not support this operation.", root), 375 Subject: traversal[1].SourceRange().Ptr(), 376 }) 377 return "", hcl.Range{}, nil, diags 378 }