]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "fmt" | |
107c1cdb ND |
5 | "log" |
6 | ||
7 | "github.com/zclconf/go-cty/cty" | |
8 | ||
9 | "github.com/hashicorp/terraform/addrs" | |
10 | "github.com/hashicorp/terraform/configs" | |
11 | "github.com/hashicorp/terraform/plans" | |
12 | "github.com/hashicorp/terraform/plans/objchange" | |
13 | "github.com/hashicorp/terraform/providers" | |
14 | "github.com/hashicorp/terraform/states" | |
15 | "github.com/hashicorp/terraform/tfdiags" | |
bae9f6d2 JC |
16 | ) |
17 | ||
107c1cdb ND |
18 | // EvalReadData is an EvalNode implementation that deals with the main part |
19 | // of the data resource lifecycle: either actually reading from the data source | |
20 | // or generating a plan to do so. | |
21 | type EvalReadData struct { | |
22 | Addr addrs.ResourceInstance | |
23 | Config *configs.Resource | |
24 | Dependencies []addrs.Referenceable | |
25 | Provider *providers.Interface | |
26 | ProviderAddr addrs.AbsProviderConfig | |
27 | ProviderSchema **ProviderSchema | |
28 | ||
29 | // Planned is set when dealing with data resources that were deferred to | |
30 | // the apply walk, to let us see what was planned. If this is set, the | |
31 | // evaluation of the config is required to produce a wholly-known | |
32 | // configuration which is consistent with the partial object included | |
33 | // in this planned change. | |
34 | Planned **plans.ResourceInstanceChange | |
35 | ||
36 | // ForcePlanRead, if true, overrides the usual behavior of immediately | |
37 | // reading from the data source where possible, instead forcing us to | |
38 | // _always_ generate a plan. This is used during the plan walk, since we | |
39 | // mustn't actually apply anything there. (The resulting state doesn't | |
40 | // get persisted) | |
41 | ForcePlanRead bool | |
42 | ||
43 | // The result from this EvalNode has a few different possibilities | |
44 | // depending on the input: | |
45 | // - If Planned is nil then we assume we're aiming to _produce_ the plan, | |
46 | // and so the following two outcomes are possible: | |
47 | // - OutputChange.Action is plans.NoOp and OutputState is the complete | |
48 | // result of reading from the data source. This is the easy path. | |
49 | // - OutputChange.Action is plans.Read and OutputState is a planned | |
50 | // object placeholder (states.ObjectPlanned). In this case, the | |
51 | // returned change must be recorded in the overral changeset and | |
52 | // eventually passed to another instance of this struct during the | |
53 | // apply walk. | |
54 | // - If Planned is non-nil then we assume we're aiming to complete a | |
55 | // planned read from an earlier plan walk. In this case the only possible | |
56 | // non-error outcome is to set Output.Action (if non-nil) to a plans.NoOp | |
57 | // change and put the complete resulting state in OutputState, ready to | |
58 | // be saved in the overall state and used for expression evaluation. | |
59 | OutputChange **plans.ResourceInstanceChange | |
60 | OutputValue *cty.Value | |
61 | OutputConfigValue *cty.Value | |
62 | OutputState **states.ResourceInstanceObject | |
bae9f6d2 JC |
63 | } |
64 | ||
107c1cdb ND |
65 | func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) { |
66 | absAddr := n.Addr.Absolute(ctx.Path()) | |
67 | log.Printf("[TRACE] EvalReadData: working on %s", absAddr) | |
bae9f6d2 | 68 | |
107c1cdb ND |
69 | if n.ProviderSchema == nil || *n.ProviderSchema == nil { |
70 | return nil, fmt.Errorf("provider schema not available for %s", n.Addr) | |
bae9f6d2 JC |
71 | } |
72 | ||
107c1cdb ND |
73 | var diags tfdiags.Diagnostics |
74 | var change *plans.ResourceInstanceChange | |
75 | var configVal cty.Value | |
bae9f6d2 | 76 | |
107c1cdb ND |
77 | // TODO: Do we need to handle Delete changes here? EvalReadDataDiff and |
78 | // EvalReadDataApply did, but it seems like we should handle that via a | |
79 | // separate mechanism since it boils down to just deleting the object from | |
80 | // the state... and we do that on every plan anyway, forcing the data | |
81 | // resource to re-read. | |
bae9f6d2 | 82 | |
107c1cdb ND |
83 | config := *n.Config |
84 | provider := *n.Provider | |
85 | providerSchema := *n.ProviderSchema | |
86 | schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) | |
87 | if schema == nil { | |
88 | // Should be caught during validation, so we don't bother with a pretty error here | |
89 | return nil, fmt.Errorf("provider %q does not support data source %q", n.ProviderAddr.ProviderConfig.Type, n.Addr.Resource.Type) | |
90 | } | |
91 | ||
92 | // We'll always start by evaluating the configuration. What we do after | |
93 | // that will depend on the evaluation result along with what other inputs | |
94 | // we were given. | |
95 | objTy := schema.ImpliedType() | |
96 | priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time | |
97 | ||
863486a6 AG |
98 | forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx) |
99 | keyData := EvalDataForInstanceKey(n.Addr.Key, forEach) | |
107c1cdb ND |
100 | |
101 | var configDiags tfdiags.Diagnostics | |
102 | configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) | |
103 | diags = diags.Append(configDiags) | |
104 | if configDiags.HasErrors() { | |
105 | return nil, diags.Err() | |
106 | } | |
107 | ||
108 | proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal) | |
109 | ||
110 | // If our configuration contains any unknown values then we must defer the | |
111 | // read to the apply phase by producing a "Read" change for this resource, | |
112 | // and a placeholder value for it in the state. | |
113 | if n.ForcePlanRead || !configVal.IsWhollyKnown() { | |
114 | // If the configuration is still unknown when we're applying a planned | |
115 | // change then that indicates a bug in Terraform, since we should have | |
116 | // everything resolved by now. | |
117 | if n.Planned != nil && *n.Planned != nil { | |
118 | return nil, fmt.Errorf( | |
119 | "configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)", | |
120 | absAddr, | |
121 | ) | |
122 | } | |
123 | if n.ForcePlanRead { | |
124 | log.Printf("[TRACE] EvalReadData: %s configuration is fully known, but we're forcing a read plan to be created", absAddr) | |
125 | } else { | |
126 | log.Printf("[TRACE] EvalReadData: %s configuration not fully known yet, so deferring to apply phase", absAddr) | |
127 | } | |
128 | ||
129 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
130 | return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal) | |
131 | }) | |
bae9f6d2 JC |
132 | if err != nil { |
133 | return nil, err | |
134 | } | |
107c1cdb ND |
135 | |
136 | change = &plans.ResourceInstanceChange{ | |
137 | Addr: absAddr, | |
138 | ProviderAddr: n.ProviderAddr, | |
139 | Change: plans.Change{ | |
140 | Action: plans.Read, | |
141 | Before: priorVal, | |
142 | After: proposedNewVal, | |
143 | }, | |
bae9f6d2 JC |
144 | } |
145 | ||
107c1cdb ND |
146 | err = ctx.Hook(func(h Hook) (HookAction, error) { |
147 | return h.PostDiff(absAddr, states.CurrentGen, change.Action, priorVal, proposedNewVal) | |
148 | }) | |
149 | if err != nil { | |
150 | return nil, err | |
151 | } | |
152 | ||
153 | if n.OutputChange != nil { | |
154 | *n.OutputChange = change | |
155 | } | |
156 | if n.OutputValue != nil { | |
157 | *n.OutputValue = change.After | |
158 | } | |
159 | if n.OutputConfigValue != nil { | |
160 | *n.OutputConfigValue = configVal | |
bae9f6d2 | 161 | } |
107c1cdb ND |
162 | if n.OutputState != nil { |
163 | state := &states.ResourceInstanceObject{ | |
164 | Value: change.After, | |
165 | Status: states.ObjectPlanned, // because the partial value in the plan must be used for now | |
166 | Dependencies: n.Dependencies, | |
167 | } | |
168 | *n.OutputState = state | |
169 | } | |
170 | ||
171 | return nil, diags.ErrWithWarnings() | |
bae9f6d2 JC |
172 | } |
173 | ||
107c1cdb ND |
174 | if n.Planned != nil && *n.Planned != nil && (*n.Planned).Action != plans.Read { |
175 | // If any other action gets in here then that's always a bug; this | |
176 | // EvalNode only deals with reading. | |
177 | return nil, fmt.Errorf( | |
178 | "invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)", | |
179 | (*n.Planned).Action, absAddr, | |
180 | ) | |
181 | } | |
182 | ||
863486a6 AG |
183 | log.Printf("[TRACE] Re-validating config for %s", absAddr) |
184 | validateResp := provider.ValidateDataSourceConfig( | |
185 | providers.ValidateDataSourceConfigRequest{ | |
186 | TypeName: n.Addr.Resource.Type, | |
187 | Config: configVal, | |
188 | }, | |
189 | ) | |
190 | if validateResp.Diagnostics.HasErrors() { | |
191 | return nil, validateResp.Diagnostics.InConfigBody(n.Config.Config).Err() | |
192 | } | |
193 | ||
107c1cdb ND |
194 | // If we get down here then our configuration is complete and we're read |
195 | // to actually call the provider to read the data. | |
196 | log.Printf("[TRACE] EvalReadData: %s configuration is complete, so reading from provider", absAddr) | |
197 | ||
198 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
199 | // We don't have a state yet, so we'll just give the hook an | |
200 | // empty one to work with. | |
201 | return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType)) | |
bae9f6d2 JC |
202 | }) |
203 | if err != nil { | |
204 | return nil, err | |
205 | } | |
206 | ||
107c1cdb ND |
207 | resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ |
208 | TypeName: n.Addr.Resource.Type, | |
209 | Config: configVal, | |
210 | }) | |
211 | diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config)) | |
212 | if diags.HasErrors() { | |
213 | return nil, diags.Err() | |
214 | } | |
215 | newVal := resp.State | |
216 | if newVal == cty.NilVal { | |
217 | // This can happen with incompletely-configured mocks. We'll allow it | |
218 | // and treat it as an alias for a properly-typed null value. | |
219 | newVal = cty.NullVal(schema.ImpliedType()) | |
220 | } | |
221 | ||
222 | for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { | |
223 | diags = diags.Append(tfdiags.Sourceless( | |
224 | tfdiags.Error, | |
225 | "Provider produced invalid object", | |
226 | fmt.Sprintf( | |
227 | "Provider %q produced an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", | |
228 | n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), | |
229 | ), | |
230 | )) | |
231 | } | |
232 | if diags.HasErrors() { | |
233 | return nil, diags.Err() | |
234 | } | |
235 | ||
236 | if newVal.IsNull() { | |
237 | diags = diags.Append(tfdiags.Sourceless( | |
238 | tfdiags.Error, | |
239 | "Provider produced null object", | |
240 | fmt.Sprintf( | |
241 | "Provider %q produced a null value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", | |
242 | n.ProviderAddr.ProviderConfig.Type, absAddr, | |
243 | ), | |
244 | )) | |
245 | } | |
246 | if !newVal.IsWhollyKnown() { | |
247 | diags = diags.Append(tfdiags.Sourceless( | |
248 | tfdiags.Error, | |
249 | "Provider produced invalid object", | |
250 | fmt.Sprintf( | |
251 | "Provider %q produced a value for %s that is not wholly known.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", | |
252 | n.ProviderAddr.ProviderConfig.Type, absAddr, | |
253 | ), | |
254 | )) | |
255 | ||
256 | // We'll still save the object, but we need to eliminate any unknown | |
257 | // values first because we can't serialize them in the state file. | |
258 | // Note that this may cause set elements to be coalesced if they | |
259 | // differed only by having unknown values, but we don't worry about | |
260 | // that here because we're saving the value only for inspection | |
261 | // purposes; the error we added above will halt the graph walk. | |
262 | newVal = cty.UnknownAsNull(newVal) | |
263 | } | |
264 | ||
265 | // Since we've completed the read, we actually have no change to make, but | |
266 | // we'll produce a NoOp one anyway to preserve the usual flow of the | |
267 | // plan phase and allow it to produce a complete plan. | |
268 | change = &plans.ResourceInstanceChange{ | |
269 | Addr: absAddr, | |
270 | ProviderAddr: n.ProviderAddr, | |
271 | Change: plans.Change{ | |
272 | Action: plans.NoOp, | |
273 | Before: newVal, | |
274 | After: newVal, | |
275 | }, | |
276 | } | |
277 | state := &states.ResourceInstanceObject{ | |
278 | Value: change.After, | |
279 | Status: states.ObjectReady, // because we completed the read from the provider | |
280 | Dependencies: n.Dependencies, | |
281 | } | |
282 | ||
283 | err = ctx.Hook(func(h Hook) (HookAction, error) { | |
284 | return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal) | |
285 | }) | |
286 | if err != nil { | |
287 | return nil, err | |
288 | } | |
bae9f6d2 | 289 | |
107c1cdb ND |
290 | if n.OutputChange != nil { |
291 | *n.OutputChange = change | |
292 | } | |
293 | if n.OutputValue != nil { | |
294 | *n.OutputValue = change.After | |
295 | } | |
296 | if n.OutputConfigValue != nil { | |
297 | *n.OutputConfigValue = configVal | |
298 | } | |
bae9f6d2 | 299 | if n.OutputState != nil { |
bae9f6d2 | 300 | *n.OutputState = state |
bae9f6d2 JC |
301 | } |
302 | ||
107c1cdb | 303 | return nil, diags.ErrWithWarnings() |
bae9f6d2 JC |
304 | } |
305 | ||
306 | // EvalReadDataApply is an EvalNode implementation that executes a data | |
307 | // resource's ReadDataApply method to read data from the data source. | |
308 | type EvalReadDataApply struct { | |
107c1cdb ND |
309 | Addr addrs.ResourceInstance |
310 | Provider *providers.Interface | |
311 | ProviderAddr addrs.AbsProviderConfig | |
312 | ProviderSchema **ProviderSchema | |
313 | Output **states.ResourceInstanceObject | |
314 | Config *configs.Resource | |
315 | Change **plans.ResourceInstanceChange | |
316 | StateReferences []addrs.Referenceable | |
bae9f6d2 JC |
317 | } |
318 | ||
319 | func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) { | |
bae9f6d2 | 320 | provider := *n.Provider |
107c1cdb ND |
321 | change := *n.Change |
322 | providerSchema := *n.ProviderSchema | |
323 | absAddr := n.Addr.Absolute(ctx.Path()) | |
324 | ||
325 | var diags tfdiags.Diagnostics | |
bae9f6d2 JC |
326 | |
327 | // If the diff is for *destroying* this resource then we'll | |
328 | // just drop its state and move on, since data resources don't | |
329 | // support an actual "destroy" action. | |
107c1cdb | 330 | if change != nil && change.Action == plans.Delete { |
bae9f6d2 JC |
331 | if n.Output != nil { |
332 | *n.Output = nil | |
333 | } | |
334 | return nil, nil | |
335 | } | |
336 | ||
337 | // For the purpose of external hooks we present a data apply as a | |
338 | // "Refresh" rather than an "Apply" because creating a data source | |
339 | // is presented to users/callers as a "read" operation. | |
340 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
341 | // We don't have a state yet, so we'll just give the hook an | |
342 | // empty one to work with. | |
107c1cdb | 343 | return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType)) |
bae9f6d2 JC |
344 | }) |
345 | if err != nil { | |
346 | return nil, err | |
347 | } | |
348 | ||
107c1cdb ND |
349 | resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ |
350 | TypeName: n.Addr.Resource.Type, | |
351 | Config: change.After, | |
352 | }) | |
353 | diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config)) | |
354 | if diags.HasErrors() { | |
355 | return nil, diags.Err() | |
356 | } | |
357 | ||
358 | schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) | |
359 | if schema == nil { | |
360 | // Should be caught during validation, so we don't bother with a pretty error here | |
361 | return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type) | |
362 | } | |
363 | ||
364 | newVal := resp.State | |
365 | for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { | |
366 | diags = diags.Append(tfdiags.Sourceless( | |
367 | tfdiags.Error, | |
368 | "Provider produced invalid object", | |
369 | fmt.Sprintf( | |
370 | "Provider %q planned an invalid value for %s. The result could not be saved.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", | |
371 | n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), | |
372 | ), | |
373 | )) | |
374 | } | |
375 | if diags.HasErrors() { | |
376 | return nil, diags.Err() | |
bae9f6d2 JC |
377 | } |
378 | ||
379 | err = ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 380 | return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal) |
bae9f6d2 JC |
381 | }) |
382 | if err != nil { | |
383 | return nil, err | |
384 | } | |
385 | ||
386 | if n.Output != nil { | |
107c1cdb ND |
387 | *n.Output = &states.ResourceInstanceObject{ |
388 | Value: newVal, | |
389 | Status: states.ObjectReady, | |
390 | Dependencies: n.StateReferences, | |
391 | } | |
bae9f6d2 JC |
392 | } |
393 | ||
107c1cdb | 394 | return nil, diags.ErrWithWarnings() |
bae9f6d2 | 395 | } |