1package terraform
2
3import (
4	"fmt"
5	"log"
6
7	"github.com/zclconf/go-cty/cty"
8
9	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
10	"github.com/hashicorp/terraform-plugin-sdk/internal/configs"
11	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
12	"github.com/hashicorp/terraform-plugin-sdk/internal/plans/objchange"
13	"github.com/hashicorp/terraform-plugin-sdk/internal/providers"
14	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
15	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
16)
17
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.
21type 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
63}
64
65func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
66	absAddr := n.Addr.Absolute(ctx.Path())
67	log.Printf("[TRACE] EvalReadData: working on %s", absAddr)
68
69	if n.ProviderSchema == nil || *n.ProviderSchema == nil {
70		return nil, fmt.Errorf("provider schema not available for %s", n.Addr)
71	}
72
73	var diags tfdiags.Diagnostics
74	var change *plans.ResourceInstanceChange
75	var configVal cty.Value
76
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.
82
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	forEach, _ := evaluateResourceForEachExpression(n.Config.ForEach, ctx)
99	keyData := EvalDataForInstanceKey(n.Addr.Key, forEach)
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		})
132		if err != nil {
133			return nil, err
134		}
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			},
144		}
145
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
161		}
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()
172	}
173
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
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
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))
202	})
203	if err != nil {
204		return nil, err
205	}
206
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	}
289
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	}
299	if n.OutputState != nil {
300		*n.OutputState = state
301	}
302
303	return nil, diags.ErrWithWarnings()
304}
305
306// EvalReadDataApply is an EvalNode implementation that executes a data
307// resource's ReadDataApply method to read data from the data source.
308type EvalReadDataApply struct {
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
317}
318
319func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
320	provider := *n.Provider
321	change := *n.Change
322	providerSchema := *n.ProviderSchema
323	absAddr := n.Addr.Absolute(ctx.Path())
324
325	var diags tfdiags.Diagnostics
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.
330	if change != nil && change.Action == plans.Delete {
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.
343		return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
344	})
345	if err != nil {
346		return nil, err
347	}
348
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()
377	}
378
379	err = ctx.Hook(func(h Hook) (HookAction, error) {
380		return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
381	})
382	if err != nil {
383		return nil, err
384	}
385
386	if n.Output != nil {
387		*n.Output = &states.ResourceInstanceObject{
388			Value:        newVal,
389			Status:       states.ObjectReady,
390			Dependencies: n.StateReferences,
391		}
392	}
393
394	return nil, diags.ErrWithWarnings()
395}
396