github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/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/hashicorp/terraform/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 "local": 110 name, rng, remain, diags := parseSingleAttrRef(traversal) 111 return &Reference{ 112 Subject: LocalValue{Name: name}, 113 SourceRange: tfdiags.SourceRangeFromHCL(rng), 114 Remaining: remain, 115 }, diags 116 117 case "module": 118 callName, callRange, remain, diags := parseSingleAttrRef(traversal) 119 if diags.HasErrors() { 120 return nil, diags 121 } 122 123 // A traversal starting with "module" can either be a reference to 124 // an entire module instance or to a single output from a module 125 // instance, depending on what we find after this introducer. 126 127 callInstance := ModuleCallInstance{ 128 Call: ModuleCall{ 129 Name: callName, 130 }, 131 Key: NoKey, 132 } 133 134 if len(remain) == 0 { 135 // Reference to an entire module instance. Might alternatively 136 // be a reference to a collection of instances of a particular 137 // module, but the caller will need to deal with that ambiguity 138 // since we don't have enough context here. 139 return &Reference{ 140 Subject: callInstance, 141 SourceRange: tfdiags.SourceRangeFromHCL(callRange), 142 Remaining: remain, 143 }, diags 144 } 145 146 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 147 var err error 148 callInstance.Key, err = ParseInstanceKey(idxTrav.Key) 149 if err != nil { 150 diags = diags.Append(&hcl.Diagnostic{ 151 Severity: hcl.DiagError, 152 Summary: "Invalid index key", 153 Detail: fmt.Sprintf("Invalid index for module instance: %s.", err), 154 Subject: &idxTrav.SrcRange, 155 }) 156 return nil, diags 157 } 158 remain = remain[1:] 159 160 if len(remain) == 0 { 161 // Also a reference to an entire module instance, but we have a key 162 // now. 163 return &Reference{ 164 Subject: callInstance, 165 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, idxTrav.SrcRange)), 166 Remaining: remain, 167 }, diags 168 } 169 } 170 171 if attrTrav, ok := remain[0].(hcl.TraverseAttr); ok { 172 remain = remain[1:] 173 return &Reference{ 174 Subject: ModuleCallOutput{ 175 Name: attrTrav.Name, 176 Call: callInstance, 177 }, 178 SourceRange: tfdiags.SourceRangeFromHCL(hcl.RangeBetween(callRange, attrTrav.SrcRange)), 179 Remaining: remain, 180 }, diags 181 } 182 183 diags = diags.Append(&hcl.Diagnostic{ 184 Severity: hcl.DiagError, 185 Summary: "Invalid reference", 186 Detail: "Module instance objects do not support this operation.", 187 Subject: remain[0].SourceRange().Ptr(), 188 }) 189 return nil, diags 190 191 case "path": 192 name, rng, remain, diags := parseSingleAttrRef(traversal) 193 return &Reference{ 194 Subject: PathAttr{Name: name}, 195 SourceRange: tfdiags.SourceRangeFromHCL(rng), 196 Remaining: remain, 197 }, diags 198 199 case "self": 200 return &Reference{ 201 Subject: Self, 202 SourceRange: tfdiags.SourceRangeFromHCL(rootRange), 203 Remaining: traversal[1:], 204 }, diags 205 206 case "terraform": 207 name, rng, remain, diags := parseSingleAttrRef(traversal) 208 return &Reference{ 209 Subject: TerraformAttr{Name: name}, 210 SourceRange: tfdiags.SourceRangeFromHCL(rng), 211 Remaining: remain, 212 }, diags 213 214 case "var": 215 name, rng, remain, diags := parseSingleAttrRef(traversal) 216 return &Reference{ 217 Subject: InputVariable{Name: name}, 218 SourceRange: tfdiags.SourceRangeFromHCL(rng), 219 Remaining: remain, 220 }, diags 221 222 default: 223 return parseResourceRef(ManagedResourceMode, rootRange, traversal) 224 } 225 } 226 227 func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { 228 var diags tfdiags.Diagnostics 229 230 if len(traversal) < 2 { 231 diags = diags.Append(&hcl.Diagnostic{ 232 Severity: hcl.DiagError, 233 Summary: "Invalid reference", 234 Detail: `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, 235 Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), 236 }) 237 return nil, diags 238 } 239 240 var typeName, name string 241 switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode 242 case hcl.TraverseRoot: 243 typeName = tt.Name 244 case hcl.TraverseAttr: 245 typeName = tt.Name 246 default: 247 // If it isn't a TraverseRoot then it must be a "data" reference. 248 diags = diags.Append(&hcl.Diagnostic{ 249 Severity: hcl.DiagError, 250 Summary: "Invalid reference", 251 Detail: `The "data" object does not support this operation.`, 252 Subject: traversal[0].SourceRange().Ptr(), 253 }) 254 return nil, diags 255 } 256 257 attrTrav, ok := traversal[1].(hcl.TraverseAttr) 258 if !ok { 259 var what string 260 switch mode { 261 case DataResourceMode: 262 what = "data source" 263 default: 264 what = "resource type" 265 } 266 diags = diags.Append(&hcl.Diagnostic{ 267 Severity: hcl.DiagError, 268 Summary: "Invalid reference", 269 Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), 270 Subject: traversal[1].SourceRange().Ptr(), 271 }) 272 return nil, diags 273 } 274 name = attrTrav.Name 275 rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) 276 remain := traversal[2:] 277 278 resourceAddr := Resource{ 279 Mode: mode, 280 Type: typeName, 281 Name: name, 282 } 283 resourceInstAddr := ResourceInstance{ 284 Resource: resourceAddr, 285 Key: NoKey, 286 } 287 288 if len(remain) == 0 { 289 // This might actually be a reference to the collection of all instances 290 // of the resource, but we don't have enough context here to decide 291 // so we'll let the caller resolve that ambiguity. 292 return &Reference{ 293 Subject: resourceAddr, 294 SourceRange: tfdiags.SourceRangeFromHCL(rng), 295 }, diags 296 } 297 298 if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { 299 var err error 300 resourceInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) 301 if err != nil { 302 diags = diags.Append(&hcl.Diagnostic{ 303 Severity: hcl.DiagError, 304 Summary: "Invalid index key", 305 Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), 306 Subject: &idxTrav.SrcRange, 307 }) 308 return nil, diags 309 } 310 remain = remain[1:] 311 rng = hcl.RangeBetween(rng, idxTrav.SrcRange) 312 } 313 314 return &Reference{ 315 Subject: resourceInstAddr, 316 SourceRange: tfdiags.SourceRangeFromHCL(rng), 317 Remaining: remain, 318 }, diags 319 } 320 321 func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { 322 var diags tfdiags.Diagnostics 323 324 root := traversal.RootName() 325 rootRange := traversal[0].SourceRange() 326 327 if len(traversal) < 2 { 328 diags = diags.Append(&hcl.Diagnostic{ 329 Severity: hcl.DiagError, 330 Summary: "Invalid reference", 331 Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), 332 Subject: &rootRange, 333 }) 334 return "", hcl.Range{}, nil, diags 335 } 336 if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { 337 return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags 338 } 339 diags = diags.Append(&hcl.Diagnostic{ 340 Severity: hcl.DiagError, 341 Summary: "Invalid reference", 342 Detail: fmt.Sprintf("The %q object does not support this operation.", root), 343 Subject: traversal[1].SourceRange().Ptr(), 344 }) 345 return "", hcl.Range{}, nil, diags 346 }