aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/terraform/eval_read_data.go
blob: 34f2d60adec83bc2104cbc36bfccce77cd1d9511 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package terraform

import (
	"fmt"
	"log"

	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/configs"
	"github.com/hashicorp/terraform/plans"
	"github.com/hashicorp/terraform/plans/objchange"
	"github.com/hashicorp/terraform/providers"
	"github.com/hashicorp/terraform/states"
	"github.com/hashicorp/terraform/tfdiags"
)

// EvalReadData is an EvalNode implementation that deals with the main part
// of the data resource lifecycle: either actually reading from the data source
// or generating a plan to do so.
type EvalReadData struct {
	Addr           addrs.ResourceInstance
	Config         *configs.Resource
	Dependencies   []addrs.Referenceable
	Provider       *providers.Interface
	ProviderAddr   addrs.AbsProviderConfig
	ProviderSchema **ProviderSchema

	// Planned is set when dealing with data resources that were deferred to
	// the apply walk, to let us see what was planned. If this is set, the
	// evaluation of the config is required to produce a wholly-known
	// configuration which is consistent with the partial object included
	// in this planned change.
	Planned **plans.ResourceInstanceChange

	// ForcePlanRead, if true, overrides the usual behavior of immediately
	// reading from the data source where possible, instead forcing us to
	// _always_ generate a plan. This is used during the plan walk, since we
	// mustn't actually apply anything there. (The resulting state doesn't
	// get persisted)
	ForcePlanRead bool

	// The result from this EvalNode has a few different possibilities
	// depending on the input:
	// - If Planned is nil then we assume we're aiming to _produce_ the plan,
	//   and so the following two outcomes are possible:
	//     - OutputChange.Action is plans.NoOp and OutputState is the complete
	//       result of reading from the data source. This is the easy path.
	//     - OutputChange.Action is plans.Read and OutputState is a planned
	//       object placeholder (states.ObjectPlanned). In this case, the
	//       returned change must be recorded in the overral changeset and
	//       eventually passed to another instance of this struct during the
	//       apply walk.
	// - If Planned is non-nil then we assume we're aiming to complete a
	//   planned read from an earlier plan walk. In this case the only possible
	//   non-error outcome is to set Output.Action (if non-nil) to a plans.NoOp
	//   change and put the complete resulting state in OutputState, ready to
	//   be saved in the overall state and used for expression evaluation.
	OutputChange      **plans.ResourceInstanceChange
	OutputValue       *cty.Value
	OutputConfigValue *cty.Value
	OutputState       **states.ResourceInstanceObject
}

func (n *EvalReadData) Eval(ctx EvalContext) (interface{}, error) {
	absAddr := n.Addr.Absolute(ctx.Path())
	log.Printf("[TRACE] EvalReadData: working on %s", absAddr)

	if n.ProviderSchema == nil || *n.ProviderSchema == nil {
		return nil, fmt.Errorf("provider schema not available for %s", n.Addr)
	}

	var diags tfdiags.Diagnostics
	var change *plans.ResourceInstanceChange
	var configVal cty.Value

	// TODO: Do we need to handle Delete changes here? EvalReadDataDiff and
	// EvalReadDataApply did, but it seems like we should handle that via a
	// separate mechanism since it boils down to just deleting the object from
	// the state... and we do that on every plan anyway, forcing the data
	// resource to re-read.

	config := *n.Config
	provider := *n.Provider
	providerSchema := *n.ProviderSchema
	schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
	if schema == nil {
		// Should be caught during validation, so we don't bother with a pretty error here
		return nil, fmt.Errorf("provider %q does not support data source %q", n.ProviderAddr.ProviderConfig.Type, n.Addr.Resource.Type)
	}

	// We'll always start by evaluating the configuration. What we do after
	// that will depend on the evaluation result along with what other inputs
	// we were given.
	objTy := schema.ImpliedType()
	priorVal := cty.NullVal(objTy) // for data resources, prior is always null because we start fresh every time

	keyData := EvalDataForInstanceKey(n.Addr.Key)

	var configDiags tfdiags.Diagnostics
	configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData)
	diags = diags.Append(configDiags)
	if configDiags.HasErrors() {
		return nil, diags.Err()
	}

	proposedNewVal := objchange.PlannedDataResourceObject(schema, configVal)

	// If our configuration contains any unknown values then we must defer the
	// read to the apply phase by producing a "Read" change for this resource,
	// and a placeholder value for it in the state.
	if n.ForcePlanRead || !configVal.IsWhollyKnown() {
		// If the configuration is still unknown when we're applying a planned
		// change then that indicates a bug in Terraform, since we should have
		// everything resolved by now.
		if n.Planned != nil && *n.Planned != nil {
			return nil, fmt.Errorf(
				"configuration for %s still contains unknown values during apply (this is a bug in Terraform; please report it!)",
				absAddr,
			)
		}
		if n.ForcePlanRead {
			log.Printf("[TRACE] EvalReadData: %s configuration is fully known, but we're forcing a read plan to be created", absAddr)
		} else {
			log.Printf("[TRACE] EvalReadData: %s configuration not fully known yet, so deferring to apply phase", absAddr)
		}

		err := ctx.Hook(func(h Hook) (HookAction, error) {
			return h.PreDiff(absAddr, states.CurrentGen, priorVal, proposedNewVal)
		})
		if err != nil {
			return nil, err
		}

		change = &plans.ResourceInstanceChange{
			Addr:         absAddr,
			ProviderAddr: n.ProviderAddr,
			Change: plans.Change{
				Action: plans.Read,
				Before: priorVal,
				After:  proposedNewVal,
			},
		}

		err = ctx.Hook(func(h Hook) (HookAction, error) {
			return h.PostDiff(absAddr, states.CurrentGen, change.Action, priorVal, proposedNewVal)
		})
		if err != nil {
			return nil, err
		}

		if n.OutputChange != nil {
			*n.OutputChange = change
		}
		if n.OutputValue != nil {
			*n.OutputValue = change.After
		}
		if n.OutputConfigValue != nil {
			*n.OutputConfigValue = configVal
		}
		if n.OutputState != nil {
			state := &states.ResourceInstanceObject{
				Value:        change.After,
				Status:       states.ObjectPlanned, // because the partial value in the plan must be used for now
				Dependencies: n.Dependencies,
			}
			*n.OutputState = state
		}

		return nil, diags.ErrWithWarnings()
	}

	if n.Planned != nil && *n.Planned != nil && (*n.Planned).Action != plans.Read {
		// If any other action gets in here then that's always a bug; this
		// EvalNode only deals with reading.
		return nil, fmt.Errorf(
			"invalid action %s for %s: only Read is supported (this is a bug in Terraform; please report it!)",
			(*n.Planned).Action, absAddr,
		)
	}

	// If we get down here then our configuration is complete and we're read
	// to actually call the provider to read the data.
	log.Printf("[TRACE] EvalReadData: %s configuration is complete, so reading from provider", absAddr)

	err := ctx.Hook(func(h Hook) (HookAction, error) {
		// We don't have a state yet, so we'll just give the hook an
		// empty one to work with.
		return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
	})
	if err != nil {
		return nil, err
	}

	resp := provider.ReadDataSource(providers.ReadDataSourceRequest{
		TypeName: n.Addr.Resource.Type,
		Config:   configVal,
	})
	diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
	if diags.HasErrors() {
		return nil, diags.Err()
	}
	newVal := resp.State
	if newVal == cty.NilVal {
		// This can happen with incompletely-configured mocks. We'll allow it
		// and treat it as an alias for a properly-typed null value.
		newVal = cty.NullVal(schema.ImpliedType())
	}

	for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Provider produced invalid object",
			fmt.Sprintf(
				"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.",
				n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
			),
		))
	}
	if diags.HasErrors() {
		return nil, diags.Err()
	}

	if newVal.IsNull() {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Provider produced null object",
			fmt.Sprintf(
				"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.",
				n.ProviderAddr.ProviderConfig.Type, absAddr,
			),
		))
	}
	if !newVal.IsWhollyKnown() {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Provider produced invalid object",
			fmt.Sprintf(
				"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.",
				n.ProviderAddr.ProviderConfig.Type, absAddr,
			),
		))

		// We'll still save the object, but we need to eliminate any unknown
		// values first because we can't serialize them in the state file.
		// Note that this may cause set elements to be coalesced if they
		// differed only by having unknown values, but we don't worry about
		// that here because we're saving the value only for inspection
		// purposes; the error we added above will halt the graph walk.
		newVal = cty.UnknownAsNull(newVal)
	}

	// Since we've completed the read, we actually have no change to make, but
	// we'll produce a NoOp one anyway to preserve the usual flow of the
	// plan phase and allow it to produce a complete plan.
	change = &plans.ResourceInstanceChange{
		Addr:         absAddr,
		ProviderAddr: n.ProviderAddr,
		Change: plans.Change{
			Action: plans.NoOp,
			Before: newVal,
			After:  newVal,
		},
	}
	state := &states.ResourceInstanceObject{
		Value:        change.After,
		Status:       states.ObjectReady, // because we completed the read from the provider
		Dependencies: n.Dependencies,
	}

	err = ctx.Hook(func(h Hook) (HookAction, error) {
		return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
	})
	if err != nil {
		return nil, err
	}

	if n.OutputChange != nil {
		*n.OutputChange = change
	}
	if n.OutputValue != nil {
		*n.OutputValue = change.After
	}
	if n.OutputConfigValue != nil {
		*n.OutputConfigValue = configVal
	}
	if n.OutputState != nil {
		*n.OutputState = state
	}

	return nil, diags.ErrWithWarnings()
}

// EvalReadDataApply is an EvalNode implementation that executes a data
// resource's ReadDataApply method to read data from the data source.
type EvalReadDataApply struct {
	Addr            addrs.ResourceInstance
	Provider        *providers.Interface
	ProviderAddr    addrs.AbsProviderConfig
	ProviderSchema  **ProviderSchema
	Output          **states.ResourceInstanceObject
	Config          *configs.Resource
	Change          **plans.ResourceInstanceChange
	StateReferences []addrs.Referenceable
}

func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
	provider := *n.Provider
	change := *n.Change
	providerSchema := *n.ProviderSchema
	absAddr := n.Addr.Absolute(ctx.Path())

	var diags tfdiags.Diagnostics

	// If the diff is for *destroying* this resource then we'll
	// just drop its state and move on, since data resources don't
	// support an actual "destroy" action.
	if change != nil && change.Action == plans.Delete {
		if n.Output != nil {
			*n.Output = nil
		}
		return nil, nil
	}

	// For the purpose of external hooks we present a data apply as a
	// "Refresh" rather than an "Apply" because creating a data source
	// is presented to users/callers as a "read" operation.
	err := ctx.Hook(func(h Hook) (HookAction, error) {
		// We don't have a state yet, so we'll just give the hook an
		// empty one to work with.
		return h.PreRefresh(absAddr, states.CurrentGen, cty.NullVal(cty.DynamicPseudoType))
	})
	if err != nil {
		return nil, err
	}

	resp := provider.ReadDataSource(providers.ReadDataSourceRequest{
		TypeName: n.Addr.Resource.Type,
		Config:   change.After,
	})
	diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config))
	if diags.HasErrors() {
		return nil, diags.Err()
	}

	schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource())
	if schema == nil {
		// Should be caught during validation, so we don't bother with a pretty error here
		return nil, fmt.Errorf("provider does not support data source %q", n.Addr.Resource.Type)
	}

	newVal := resp.State
	for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) {
		diags = diags.Append(tfdiags.Sourceless(
			tfdiags.Error,
			"Provider produced invalid object",
			fmt.Sprintf(
				"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.",
				n.ProviderAddr.ProviderConfig.Type, tfdiags.FormatErrorPrefixed(err, absAddr.String()),
			),
		))
	}
	if diags.HasErrors() {
		return nil, diags.Err()
	}

	err = ctx.Hook(func(h Hook) (HookAction, error) {
		return h.PostRefresh(absAddr, states.CurrentGen, change.Before, newVal)
	})
	if err != nil {
		return nil, err
	}

	if n.Output != nil {
		*n.Output = &states.ResourceInstanceObject{
			Value:        newVal,
			Status:       states.ObjectReady,
			Dependencies: n.StateReferences,
		}
	}

	return nil, diags.ErrWithWarnings()
}