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