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