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