diff options
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/tfdiags/contextual.go')
-rw-r--r-- | vendor/github.com/hashicorp/terraform/tfdiags/contextual.go | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/tfdiags/contextual.go b/vendor/github.com/hashicorp/terraform/tfdiags/contextual.go new file mode 100644 index 0000000..25b2140 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/tfdiags/contextual.go | |||
@@ -0,0 +1,372 @@ | |||
1 | package tfdiags | ||
2 | |||
3 | import ( | ||
4 | "github.com/hashicorp/hcl2/hcl" | ||
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 | } | ||