]>
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 | ||
98 | keyData := EvalDataForInstanceKey(n.Addr.Key) | |
99 | ||
100 | var configDiags tfdiags.Diagnostics | |
101 | configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) | |
102 | diags = diags.Append(configDiags) | |
103 | if configDiags.HasErrors() { | |
104 | return nil, diags.Err() | |
105 | } | |
106 | ||
107 | proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal) | |
108 | ||
109 | // If our configuration contains any unknown values then we must defer the | |
110 | // read to the apply phase by producing a "Read" change for this resource, | |
111 | // and a placeholder value for it in the state. | |
112 | if n.ForcePlanRead || !configVal.IsWhollyKnown() { | |
113 | // If the configuration is still unknown when we're applying a planned | |
114 | // change then that indicates a bug in Terraform, since we should have | |
115 | // everything resolved by now. | |
116 | if n.Planned != nil && *n.Planned != nil { | |
117 | return nil, fmt.Errorf( | |
118 | "configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)", | |
119 | absAddr, | |
120 | ) | |
121 | } | |
122 | if n.ForcePlanRead { | |
123 | log.Printf("[TRACE] EvalReadData: %s configuration is fully known, but we're forcing a read plan to be created", absAddr) | |
124 | } else { | |
125 | log.Printf("[TRACE] EvalReadData: %s configuration not fully known yet, so deferring to apply phase", absAddr) | |
126 | } | |
127 | ||
128 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
129 | return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal) | |
130 | }) | |
bae9f6d2 JC |
131 | if err != nil { |
132 | return nil, err | |
133 | } | |
107c1cdb ND |
134 | |
135 | change = &plans.ResourceInstanceChange{ | |
136 | Addr: absAddr, | |
137 | ProviderAddr: n.ProviderAddr, | |
138 | Change: plans.Change{ | |
139 | Action: plans.Read, | |
140 | Before: priorVal, | |
141 | After: proposedNewVal, | |
142 | }, | |
bae9f6d2 JC |
143 | } |
144 | ||
107c1cdb ND |
145 | err = ctx.Hook(func(h Hook) (HookAction, error) { |
146 | return h.PostDiff(absAddr, states.CurrentGen, change.Action, priorVal, proposedNewVal) | |
147 | }) | |
148 | if err != nil { | |
149 | return nil, err | |
150 | } | |
151 | ||
152 | if n.OutputChange != nil { | |
153 | *n.OutputChange = change | |
154 | } | |
155 | if n.OutputValue != nil { | |
156 | *n.OutputValue = change.After | |
157 | } | |
158 | if n.OutputConfigValue != nil { | |
159 | *n.OutputConfigValue = configVal | |
bae9f6d2 | 160 | } |
107c1cdb ND |
161 | if n.OutputState != nil { |
162 | state := &states.ResourceInstanceObject{ | |
163 | Value: change.After, | |
164 | Status: states.ObjectPlanned, // because the partial value in the plan must be used for now | |
165 | Dependencies: n.Dependencies, | |
166 | } | |
167 | *n.OutputState = state | |
168 | } | |
169 | ||
170 | return nil, diags.ErrWithWarnings() | |
bae9f6d2 JC |
171 | } |
172 | ||
107c1cdb ND |
173 | if n.Planned != nil && *n.Planned != nil && (*n.Planned).Action != plans.Read { |
174 | // If any other action gets in here then that's always a bug; this | |
175 | // EvalNode only deals with reading. | |
176 | return nil, fmt.Errorf( | |
177 | "invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)", | |
178 | (*n.Planned).Action, absAddr, | |
179 | ) | |
180 | } | |
181 | ||
182 | // If we get down here then our configuration is complete and we're read | |
183 | // to actually call the provider to read the data. | |
184 | log.Printf("[TRACE] EvalReadData: %s configuration is complete, so reading from provider", absAddr) | |
185 | ||
186 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
187 | // We don't have a state yet, so we'll just give the hook an | |
188 | // empty one to work with. | |
189 | return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType)) | |
bae9f6d2 JC |
190 | }) |
191 | if err != nil { | |
192 | return nil, err | |
193 | } | |
194 | ||
107c1cdb ND |
195 | resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ |
196 | TypeName: n.Addr.Resource.Type, | |
197 | Config: configVal, | |
198 | }) | |
199 | diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config)) | |
200 | if diags.HasErrors() { | |
201 | return nil, diags.Err() | |
202 | } | |
203 | newVal := resp.State | |
204 | if newVal == cty.NilVal { | |
205 | // This can happen with incompletely-configured mocks. We'll allow it | |
206 | // and treat it as an alias for a properly-typed null value. | |
207 | newVal = cty.NullVal(schema.ImpliedType()) | |
208 | } | |
209 | ||
210 | for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { | |
211 | diags = diags.Append(tfdiags.Sourceless( | |
212 | tfdiags.Error, | |
213 | "Provider produced invalid object", | |
214 | fmt.Sprintf( | |
215 | "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.", | |
216 | n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), | |
217 | ), | |
218 | )) | |
219 | } | |
220 | if diags.HasErrors() { | |
221 | return nil, diags.Err() | |
222 | } | |
223 | ||
224 | if newVal.IsNull() { | |
225 | diags = diags.Append(tfdiags.Sourceless( | |
226 | tfdiags.Error, | |
227 | "Provider produced null object", | |
228 | fmt.Sprintf( | |
229 | "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.", | |
230 | n.ProviderAddr.ProviderConfig.Type, absAddr, | |
231 | ), | |
232 | )) | |
233 | } | |
234 | if !newVal.IsWhollyKnown() { | |
235 | diags = diags.Append(tfdiags.Sourceless( | |
236 | tfdiags.Error, | |
237 | "Provider produced invalid object", | |
238 | fmt.Sprintf( | |
239 | "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.", | |
240 | n.ProviderAddr.ProviderConfig.Type, absAddr, | |
241 | ), | |
242 | )) | |
243 | ||
244 | // We'll still save the object, but we need to eliminate any unknown | |
245 | // values first because we can't serialize them in the state file. | |
246 | // Note that this may cause set elements to be coalesced if they | |
247 | // differed only by having unknown values, but we don't worry about | |
248 | // that here because we're saving the value only for inspection | |
249 | // purposes; the error we added above will halt the graph walk. | |
250 | newVal = cty.UnknownAsNull(newVal) | |
251 | } | |
252 | ||
253 | // Since we've completed the read, we actually have no change to make, but | |
254 | // we'll produce a NoOp one anyway to preserve the usual flow of the | |
255 | // plan phase and allow it to produce a complete plan. | |
256 | change = &plans.ResourceInstanceChange{ | |
257 | Addr: absAddr, | |
258 | ProviderAddr: n.ProviderAddr, | |
259 | Change: plans.Change{ | |
260 | Action: plans.NoOp, | |
261 | Before: newVal, | |
262 | After: newVal, | |
263 | }, | |
264 | } | |
265 | state := &states.ResourceInstanceObject{ | |
266 | Value: change.After, | |
267 | Status: states.ObjectReady, // because we completed the read from the provider | |
268 | Dependencies: n.Dependencies, | |
269 | } | |
270 | ||
271 | err = ctx.Hook(func(h Hook) (HookAction, error) { | |
272 | return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal) | |
273 | }) | |
274 | if err != nil { | |
275 | return nil, err | |
276 | } | |
bae9f6d2 | 277 | |
107c1cdb ND |
278 | if n.OutputChange != nil { |
279 | *n.OutputChange = change | |
280 | } | |
281 | if n.OutputValue != nil { | |
282 | *n.OutputValue = change.After | |
283 | } | |
284 | if n.OutputConfigValue != nil { | |
285 | *n.OutputConfigValue = configVal | |
286 | } | |
bae9f6d2 | 287 | if n.OutputState != nil { |
bae9f6d2 | 288 | *n.OutputState = state |
bae9f6d2 JC |
289 | } |
290 | ||
107c1cdb | 291 | return nil, diags.ErrWithWarnings() |
bae9f6d2 JC |
292 | } |
293 | ||
294 | // EvalReadDataApply is an EvalNode implementation that executes a data | |
295 | // resource's ReadDataApply method to read data from the data source. | |
296 | type EvalReadDataApply struct { | |
107c1cdb ND |
297 | Addr addrs.ResourceInstance |
298 | Provider *providers.Interface | |
299 | ProviderAddr addrs.AbsProviderConfig | |
300 | ProviderSchema **ProviderSchema | |
301 | Output **states.ResourceInstanceObject | |
302 | Config *configs.Resource | |
303 | Change **plans.ResourceInstanceChange | |
304 | StateReferences []addrs.Referenceable | |
bae9f6d2 JC |
305 | } |
306 | ||
307 | func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) { | |
bae9f6d2 | 308 | provider := *n.Provider |
107c1cdb ND |
309 | change := *n.Change |
310 | providerSchema := *n.ProviderSchema | |
311 | absAddr := n.Addr.Absolute(ctx.Path()) | |
312 | ||
313 | var diags tfdiags.Diagnostics | |
bae9f6d2 JC |
314 | |
315 | // If the diff is for *destroying* this resource then we'll | |
316 | // just drop its state and move on, since data resources don't | |
317 | // support an actual "destroy" action. | |
107c1cdb | 318 | if change != nil && change.Action == plans.Delete { |
bae9f6d2 JC |
319 | if n.Output != nil { |
320 | *n.Output = nil | |
321 | } | |
322 | return nil, nil | |
323 | } | |
324 | ||
325 | // For the purpose of external hooks we present a data apply as a | |
326 | // "Refresh" rather than an "Apply" because creating a data source | |
327 | // is presented to users/callers as a "read" operation. | |
328 | err := ctx.Hook(func(h Hook) (HookAction, error) { | |
329 | // We don't have a state yet, so we'll just give the hook an | |
330 | // empty one to work with. | |
107c1cdb | 331 | return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType)) |
bae9f6d2 JC |
332 | }) |
333 | if err != nil { | |
334 | return nil, err | |
335 | } | |
336 | ||
107c1cdb ND |
337 | resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ |
338 | TypeName: n.Addr.Resource.Type, | |
339 | Config: change.After, | |
340 | }) | |
341 | diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config)) | |
342 | if diags.HasErrors() { | |
343 | return nil, diags.Err() | |
344 | } | |
345 | ||
346 | schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource()) | |
347 | if schema == nil { | |
348 | // Should be caught during validation, so we don't bother with a pretty error here | |
349 | return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type) | |
350 | } | |
351 | ||
352 | newVal := resp.State | |
353 | for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { | |
354 | diags = diags.Append(tfdiags.Sourceless( | |
355 | tfdiags.Error, | |
356 | "Provider produced invalid object", | |
357 | fmt.Sprintf( | |
358 | "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.", | |
359 | n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()), | |
360 | ), | |
361 | )) | |
362 | } | |
363 | if diags.HasErrors() { | |
364 | return nil, diags.Err() | |
bae9f6d2 JC |
365 | } |
366 | ||
367 | err = ctx.Hook(func(h Hook) (HookAction, error) { | |
107c1cdb | 368 | return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal) |
bae9f6d2 JC |
369 | }) |
370 | if err != nil { | |
371 | return nil, err | |
372 | } | |
373 | ||
374 | if n.Output != nil { | |
107c1cdb ND |
375 | *n.Output = &states.ResourceInstanceObject{ |
376 | Value: newVal, | |
377 | Status: states.ObjectReady, | |
378 | Dependencies: n.StateReferences, | |
379 | } | |
bae9f6d2 JC |
380 | } |
381 | ||
107c1cdb | 382 | return nil, diags.ErrWithWarnings() |
bae9f6d2 | 383 | } |