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