github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tfdiags/contextual.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package tfdiags 5 6 import ( 7 "github.com/hashicorp/hcl/v2" 8 "github.com/zclconf/go-cty/cty" 9 "github.com/zclconf/go-cty/cty/gocty" 10 ) 11 12 // The "contextual" family of diagnostics are designed to allow separating 13 // the detection of a problem from placing that problem in context. For 14 // example, some code that is validating an object extracted from configuration 15 // may not have access to the configuration that generated it, but can still 16 // report problems within that object which the caller can then place in 17 // context by calling IsConfigBody on the returned diagnostics. 18 // 19 // When contextual diagnostics are used, the documentation for a method must 20 // be very explicit about what context is implied for any diagnostics returned, 21 // to help ensure the expected result. 22 23 // contextualFromConfig is an interface type implemented by diagnostic types 24 // that can elaborate themselves when given information about the configuration 25 // body they are embedded in, as well as the runtime address associated with 26 // that configuration. 27 // 28 // Usually this entails extracting source location information in order to 29 // populate the "Subject" range. 30 type contextualFromConfigBody interface { 31 ElaborateFromConfigBody(hcl.Body, string) Diagnostic 32 } 33 34 // InConfigBody returns a copy of the receiver with any config-contextual 35 // diagnostics elaborated in the context of the given body. An optional address 36 // argument may be added to indicate which instance of the configuration the 37 // error related to. 38 func (diags Diagnostics) InConfigBody(body hcl.Body, addr string) Diagnostics { 39 if len(diags) == 0 { 40 return nil 41 } 42 43 ret := make(Diagnostics, len(diags)) 44 for i, srcDiag := range diags { 45 if cd, isCD := srcDiag.(contextualFromConfigBody); isCD { 46 ret[i] = cd.ElaborateFromConfigBody(body, addr) 47 } else { 48 ret[i] = srcDiag 49 } 50 } 51 52 return ret 53 } 54 55 // AttributeValue returns a diagnostic about an attribute value in an implied current 56 // configuration context. This should be returned only from functions whose 57 // interface specifies a clear configuration context that this will be 58 // resolved in. 59 // 60 // The given path is relative to the implied configuration context. To describe 61 // a top-level attribute, it should be a single-element cty.Path with a 62 // cty.GetAttrStep. It's assumed that the path is returning into a structure 63 // that would be produced by our conventions in the configschema package; it 64 // may return unexpected results for structures that can't be represented by 65 // configschema. 66 // 67 // Since mapping attribute paths back onto configuration is an imprecise 68 // operation (e.g. dynamic block generation may cause the same block to be 69 // evaluated multiple times) the diagnostic detail should include the attribute 70 // name and other context required to help the user understand what is being 71 // referenced in case the identified source range is not unique. 72 // 73 // The returned attribute will not have source location information until 74 // context is applied to the containing diagnostics using diags.InConfigBody. 75 // After context is applied, the source location is the value assigned to the 76 // named attribute, or the containing body's "missing item range" if no 77 // value is present. 78 func AttributeValue(severity Severity, summary, detail string, attrPath cty.Path) Diagnostic { 79 return &attributeDiagnostic{ 80 diagnosticBase: diagnosticBase{ 81 severity: severity, 82 summary: summary, 83 detail: detail, 84 }, 85 attrPath: attrPath, 86 } 87 } 88 89 // GetAttribute extracts an attribute cty.Path from a diagnostic if it contains 90 // one. Normally this is not accessed directly, and instead the config body is 91 // added to the Diagnostic to create a more complete message for the user. In 92 // some cases however, we may want to know just the name of the attribute that 93 // generated the Diagnostic message. 94 // This returns a nil cty.Path if it does not exist in the Diagnostic. 95 func GetAttribute(d Diagnostic) cty.Path { 96 if d, ok := d.(*attributeDiagnostic); ok { 97 return d.attrPath 98 } 99 return nil 100 } 101 102 type attributeDiagnostic struct { 103 diagnosticBase 104 attrPath cty.Path 105 subject *SourceRange // populated only after ElaborateFromConfigBody 106 } 107 108 // ElaborateFromConfigBody finds the most accurate possible source location 109 // for a diagnostic's attribute path within the given body. 110 // 111 // Backing out from a path back to a source location is not always entirely 112 // possible because we lose some information in the decoding process, so 113 // if an exact position cannot be found then the returned diagnostic will 114 // refer to a position somewhere within the containing body, which is assumed 115 // to be better than no location at all. 116 // 117 // If possible it is generally better to report an error at a layer where 118 // source location information is still available, for more accuracy. This 119 // is not always possible due to system architecture, so this serves as a 120 // "best effort" fallback behavior for such situations. 121 func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string) Diagnostic { 122 // don't change an existing address 123 if d.address == "" { 124 d.address = addr 125 } 126 127 if len(d.attrPath) < 1 { 128 // Should never happen, but we'll allow it rather than crashing. 129 return d 130 } 131 132 if d.subject != nil { 133 // Don't modify an already-elaborated diagnostic. 134 return d 135 } 136 137 ret := *d 138 139 // This function will often end up re-decoding values that were already 140 // decoded by an earlier step. This is non-ideal but is architecturally 141 // more convenient than arranging for source location information to be 142 // propagated to every place in Terraform, and this happens only in the 143 // presence of errors where performance isn't a concern. 144 145 traverse := d.attrPath[:] 146 final := d.attrPath[len(d.attrPath)-1] 147 148 // Index should never be the first step 149 // as indexing of top blocks (such as resources & data sources) 150 // is handled elsewhere 151 if _, isIdxStep := traverse[0].(cty.IndexStep); isIdxStep { 152 subject := SourceRangeFromHCL(body.MissingItemRange()) 153 ret.subject = &subject 154 return &ret 155 } 156 157 // Process index separately 158 idxStep, hasIdx := final.(cty.IndexStep) 159 if hasIdx { 160 final = d.attrPath[len(d.attrPath)-2] 161 traverse = d.attrPath[:len(d.attrPath)-1] 162 } 163 164 // If we have more than one step after removing index 165 // then we'll first try to traverse to a child body 166 // corresponding to the requested path. 167 if len(traverse) > 1 { 168 body = traversePathSteps(traverse, body) 169 } 170 171 // Default is to indicate a missing item in the deepest body we reached 172 // while traversing. 173 subject := SourceRangeFromHCL(body.MissingItemRange()) 174 ret.subject = &subject 175 176 // Once we get here, "final" should be a GetAttr step that maps to an 177 // attribute in our current body. 178 finalStep, isAttr := final.(cty.GetAttrStep) 179 if !isAttr { 180 return &ret 181 } 182 183 content, _, contentDiags := body.PartialContent(&hcl.BodySchema{ 184 Attributes: []hcl.AttributeSchema{ 185 { 186 Name: finalStep.Name, 187 Required: true, 188 }, 189 }, 190 }) 191 if contentDiags.HasErrors() { 192 return &ret 193 } 194 195 if attr, ok := content.Attributes[finalStep.Name]; ok { 196 hclRange := attr.Expr.Range() 197 if hasIdx { 198 // Try to be more precise by finding index range 199 hclRange = hclRangeFromIndexStepAndAttribute(idxStep, attr) 200 } 201 subject = SourceRangeFromHCL(hclRange) 202 ret.subject = &subject 203 } 204 205 return &ret 206 } 207 208 func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body { 209 for i := 0; i < len(traverse); i++ { 210 step := traverse[i] 211 212 switch tStep := step.(type) { 213 case cty.GetAttrStep: 214 215 var next cty.PathStep 216 if i < (len(traverse) - 1) { 217 next = traverse[i+1] 218 } 219 220 // Will be indexing into our result here? 221 var indexType cty.Type 222 var indexVal cty.Value 223 if nextIndex, ok := next.(cty.IndexStep); ok { 224 indexVal = nextIndex.Key 225 indexType = indexVal.Type() 226 i++ // skip over the index on subsequent iterations 227 } 228 229 var blockLabelNames []string 230 if indexType == cty.String { 231 // Map traversal means we expect one label for the key. 232 blockLabelNames = []string{"key"} 233 } 234 235 // For intermediate steps we expect to be referring to a child 236 // block, so we'll attempt decoding under that assumption. 237 content, _, contentDiags := body.PartialContent(&hcl.BodySchema{ 238 Blocks: []hcl.BlockHeaderSchema{ 239 { 240 Type: tStep.Name, 241 LabelNames: blockLabelNames, 242 }, 243 }, 244 }) 245 if contentDiags.HasErrors() { 246 return body 247 } 248 filtered := make([]*hcl.Block, 0, len(content.Blocks)) 249 for _, block := range content.Blocks { 250 if block.Type == tStep.Name { 251 filtered = append(filtered, block) 252 } 253 } 254 if len(filtered) == 0 { 255 // Step doesn't refer to a block 256 continue 257 } 258 259 switch indexType { 260 case cty.NilType: // no index at all 261 if len(filtered) != 1 { 262 return body 263 } 264 body = filtered[0].Body 265 case cty.Number: 266 var idx int 267 err := gocty.FromCtyValue(indexVal, &idx) 268 if err != nil || idx >= len(filtered) { 269 return body 270 } 271 body = filtered[idx].Body 272 case cty.String: 273 key := indexVal.AsString() 274 var block *hcl.Block 275 for _, candidate := range filtered { 276 if candidate.Labels[0] == key { 277 block = candidate 278 break 279 } 280 } 281 if block == nil { 282 // No block with this key, so we'll just indicate a 283 // missing item in the containing block. 284 return body 285 } 286 body = block.Body 287 default: 288 // Should never happen, because only string and numeric indices 289 // are supported by cty collections. 290 return body 291 } 292 293 default: 294 // For any other kind of step, we'll just return our current body 295 // as the subject and accept that this is a little inaccurate. 296 return body 297 } 298 } 299 return body 300 } 301 302 func hclRangeFromIndexStepAndAttribute(idxStep cty.IndexStep, attr *hcl.Attribute) hcl.Range { 303 switch idxStep.Key.Type() { 304 case cty.Number: 305 var idx int 306 err := gocty.FromCtyValue(idxStep.Key, &idx) 307 items, diags := hcl.ExprList(attr.Expr) 308 if diags.HasErrors() { 309 return attr.Expr.Range() 310 } 311 if err != nil || idx >= len(items) { 312 return attr.NameRange 313 } 314 return items[idx].Range() 315 case cty.String: 316 pairs, diags := hcl.ExprMap(attr.Expr) 317 if diags.HasErrors() { 318 return attr.Expr.Range() 319 } 320 stepKey := idxStep.Key.AsString() 321 for _, kvPair := range pairs { 322 key, diags := kvPair.Key.Value(nil) 323 if diags.HasErrors() { 324 return attr.Expr.Range() 325 } 326 if key.AsString() == stepKey { 327 startRng := kvPair.Value.StartRange() 328 return startRng 329 } 330 } 331 return attr.NameRange 332 } 333 return attr.Expr.Range() 334 } 335 336 func (d *attributeDiagnostic) Source() Source { 337 return Source{ 338 Subject: d.subject, 339 } 340 } 341 342 // WholeContainingBody returns a diagnostic about the body that is an implied 343 // current configuration context. This should be returned only from 344 // functions whose interface specifies a clear configuration context that this 345 // will be resolved in. 346 // 347 // The returned attribute will not have source location information until 348 // context is applied to the containing diagnostics using diags.InConfigBody. 349 // After context is applied, the source location is currently the missing item 350 // range of the body. In future, this may change to some other suitable 351 // part of the containing body. 352 func WholeContainingBody(severity Severity, summary, detail string) Diagnostic { 353 return &wholeBodyDiagnostic{ 354 diagnosticBase: diagnosticBase{ 355 severity: severity, 356 summary: summary, 357 detail: detail, 358 }, 359 } 360 } 361 362 type wholeBodyDiagnostic struct { 363 diagnosticBase 364 subject *SourceRange // populated only after ElaborateFromConfigBody 365 } 366 367 func (d *wholeBodyDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string) Diagnostic { 368 // don't change an existing address 369 if d.address == "" { 370 d.address = addr 371 } 372 373 if d.subject != nil { 374 // Don't modify an already-elaborated diagnostic. 375 return d 376 } 377 378 ret := *d 379 rng := SourceRangeFromHCL(body.MissingItemRange()) 380 ret.subject = &rng 381 return &ret 382 } 383 384 func (d *wholeBodyDiagnostic) Source() Source { 385 return Source{ 386 Subject: d.subject, 387 } 388 }