aboutsummaryrefslogblamecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/lang/eval.go
blob: a3fb363ec70682dc92659132038e50c83110bea8 (plain) (tree)
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477




























































































































































































































































































































































































































































































                                                                                                                                                                                   
package lang

import (
	"fmt"
	"log"
	"strconv"

	"github.com/hashicorp/hcl2/ext/dynblock"
	"github.com/hashicorp/hcl2/hcl"
	"github.com/hashicorp/hcl2/hcldec"
	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/lang/blocktoattr"
	"github.com/hashicorp/terraform/tfdiags"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

// ExpandBlock expands any "dynamic" blocks present in the given body. The
// result is a body with those blocks expanded, ready to be evaluated with
// EvalBlock.
//
// If the returned diagnostics contains errors then the result may be
// incomplete or invalid.
func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) {
	spec := schema.DecoderSpec()

	traversals := dynblock.ExpandVariablesHCLDec(body, spec)
	refs, diags := References(traversals)

	ctx, ctxDiags := s.EvalContext(refs)
	diags = diags.Append(ctxDiags)

	return dynblock.Expand(body, ctx), diags
}

// EvalBlock evaluates the given body using the given block schema and returns
// a cty object value representing its contents. The type of the result conforms
// to the implied type of the given schema.
//
// This function does not automatically expand "dynamic" blocks within the
// body. If that is desired, first call the ExpandBlock method to obtain
// an expanded body to pass to this method.
//
// If the returned diagnostics contains errors then the result may be
// incomplete or invalid.
func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) {
	spec := schema.DecoderSpec()

	refs, diags := ReferencesInBlock(body, schema)

	ctx, ctxDiags := s.EvalContext(refs)
	diags = diags.Append(ctxDiags)
	if diags.HasErrors() {
		// We'll stop early if we found problems in the references, because
		// it's likely evaluation will produce redundant copies of the same errors.
		return cty.UnknownVal(schema.ImpliedType()), diags
	}

	// HACK: In order to remain compatible with some assumptions made in
	// Terraform v0.11 and earlier about the approximate equivalence of
	// attribute vs. block syntax, we do a just-in-time fixup here to allow
	// any attribute in the schema that has a list-of-objects or set-of-objects
	// kind to potentially be populated instead by one or more nested blocks
	// whose type is the attribute name.
	body = blocktoattr.FixUpBlockAttrs(body, schema)

	val, evalDiags := hcldec.Decode(body, spec, ctx)
	diags = diags.Append(evalDiags)

	return val, diags
}

// EvalExpr evaluates a single expression in the receiving context and returns
// the resulting value. The value will be converted to the given type before
// it is returned if possible, or else an error diagnostic will be produced
// describing the conversion error.
//
// Pass an expected type of cty.DynamicPseudoType to skip automatic conversion
// and just obtain the returned value directly.
//
// If the returned diagnostics contains errors then the result may be
// incomplete, but will always be of the requested type.
func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
	refs, diags := ReferencesInExpr(expr)

	ctx, ctxDiags := s.EvalContext(refs)
	diags = diags.Append(ctxDiags)
	if diags.HasErrors() {
		// We'll stop early if we found problems in the references, because
		// it's likely evaluation will produce redundant copies of the same errors.
		return cty.UnknownVal(wantType), diags
	}

	val, evalDiags := expr.Value(ctx)
	diags = diags.Append(evalDiags)

	if wantType != cty.DynamicPseudoType {
		var convErr error
		val, convErr = convert.Convert(val, wantType)
		if convErr != nil {
			val = cty.UnknownVal(wantType)
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Incorrect value type",
				Detail:   fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)),
				Subject:  expr.Range().Ptr(),
			})
		}
	}

	return val, diags
}

// EvalReference evaluates the given reference in the receiving scope and
// returns the resulting value. The value will be converted to the given type before
// it is returned if possible, or else an error diagnostic will be produced
// describing the conversion error.
//
// Pass an expected type of cty.DynamicPseudoType to skip automatic conversion
// and just obtain the returned value directly.
//
// If the returned diagnostics contains errors then the result may be
// incomplete, but will always be of the requested type.
func (s *Scope) EvalReference(ref *addrs.Reference, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
	var diags tfdiags.Diagnostics

	// We cheat a bit here and just build an EvalContext for our requested
	// reference with the "self" address overridden, and then pull the "self"
	// result out of it to return.
	ctx, ctxDiags := s.evalContext([]*addrs.Reference{ref}, ref.Subject)
	diags = diags.Append(ctxDiags)
	val := ctx.Variables["self"]
	if val == cty.NilVal {
		val = cty.DynamicVal
	}

	var convErr error
	val, convErr = convert.Convert(val, wantType)
	if convErr != nil {
		val = cty.UnknownVal(wantType)
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Incorrect value type",
			Detail:   fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)),
			Subject:  ref.SourceRange.ToHCL().Ptr(),
		})
	}

	return val, diags
}

// EvalContext constructs a HCL expression evaluation context whose variable
// scope contains sufficient values to satisfy the given set of references.
//
// Most callers should prefer to use the evaluation helper methods that
// this type offers, but this is here for less common situations where the
// caller will handle the evaluation calls itself.
func (s *Scope) EvalContext(refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
	return s.evalContext(refs, s.SelfAddr)
}

func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceable) (*hcl.EvalContext, tfdiags.Diagnostics) {
	if s == nil {
		panic("attempt to construct EvalContext for nil Scope")
	}

	var diags tfdiags.Diagnostics
	vals := make(map[string]cty.Value)
	funcs := s.Functions()
	ctx := &hcl.EvalContext{
		Variables: vals,
		Functions: funcs,
	}

	if len(refs) == 0 {
		// Easy path for common case where there are no references at all.
		return ctx, diags
	}

	// First we'll do static validation of the references. This catches things
	// early that might otherwise not get caught due to unknown values being
	// present in the scope during planning.
	if staticDiags := s.Data.StaticValidateReferences(refs, selfAddr); staticDiags.HasErrors() {
		diags = diags.Append(staticDiags)
		return ctx, diags
	}

	// The reference set we are given has not been de-duped, and so there can
	// be redundant requests in it for two reasons:
	//  - The same item is referenced multiple times
	//  - Both an item and that item's container are separately referenced.
	// We will still visit every reference here and ask our data source for
	// it, since that allows us to gather a full set of any errors and
	// warnings, but once we've gathered all the data we'll then skip anything
	// that's redundant in the process of populating our values map.
	dataResources := map[string]map[string]map[addrs.InstanceKey]cty.Value{}
	managedResources := map[string]map[string]map[addrs.InstanceKey]cty.Value{}
	wholeModules := map[string]map[addrs.InstanceKey]cty.Value{}
	moduleOutputs := map[string]map[addrs.InstanceKey]map[string]cty.Value{}
	inputVariables := map[string]cty.Value{}
	localValues := map[string]cty.Value{}
	pathAttrs := map[string]cty.Value{}
	terraformAttrs := map[string]cty.Value{}
	countAttrs := map[string]cty.Value{}
	var self cty.Value

	for _, ref := range refs {
		rng := ref.SourceRange
		isSelf := false

		rawSubj := ref.Subject
		if rawSubj == addrs.Self {
			if selfAddr == nil {
				diags = diags.Append(&hcl.Diagnostic{
					Severity: hcl.DiagError,
					Summary:  `Invalid "self" reference`,
					// This detail message mentions some current practice that
					// this codepath doesn't really "know about". If the "self"
					// object starts being supported in more contexts later then
					// we'll need to adjust this message.
					Detail:  `The "self" object is not available in this context. This object can be used only in resource provisioner and connection blocks.`,
					Subject: ref.SourceRange.ToHCL().Ptr(),
				})
				continue
			}

			// Treat "self" as an alias for the configured self address.
			rawSubj = selfAddr
			isSelf = true

			if rawSubj == addrs.Self {
				// Programming error: the self address cannot alias itself.
				panic("scope SelfAddr attempting to alias itself")
			}
		}

		// This type switch must cover all of the "Referenceable" implementations
		// in package addrs.
		switch subj := rawSubj.(type) {

		case addrs.ResourceInstance:
			var into map[string]map[string]map[addrs.InstanceKey]cty.Value
			switch subj.Resource.Mode {
			case addrs.ManagedResourceMode:
				into = managedResources
			case addrs.DataResourceMode:
				into = dataResources
			default:
				panic(fmt.Errorf("unsupported ResourceMode %s", subj.Resource.Mode))
			}

			val, valDiags := normalizeRefValue(s.Data.GetResourceInstance(subj, rng))
			diags = diags.Append(valDiags)

			r := subj.Resource
			if into[r.Type] == nil {
				into[r.Type] = make(map[string]map[addrs.InstanceKey]cty.Value)
			}
			if into[r.Type][r.Name] == nil {
				into[r.Type][r.Name] = make(map[addrs.InstanceKey]cty.Value)
			}
			into[r.Type][r.Name][subj.Key] = val
			if isSelf {
				self = val
			}

		case addrs.ModuleCallInstance:
			val, valDiags := normalizeRefValue(s.Data.GetModuleInstance(subj, rng))
			diags = diags.Append(valDiags)

			if wholeModules[subj.Call.Name] == nil {
				wholeModules[subj.Call.Name] = make(map[addrs.InstanceKey]cty.Value)
			}
			wholeModules[subj.Call.Name][subj.Key] = val
			if isSelf {
				self = val
			}

		case addrs.ModuleCallOutput:
			val, valDiags := normalizeRefValue(s.Data.GetModuleInstanceOutput(subj, rng))
			diags = diags.Append(valDiags)

			callName := subj.Call.Call.Name
			callKey := subj.Call.Key
			if moduleOutputs[callName] == nil {
				moduleOutputs[callName] = make(map[addrs.InstanceKey]map[string]cty.Value)
			}
			if moduleOutputs[callName][callKey] == nil {
				moduleOutputs[callName][callKey] = make(map[string]cty.Value)
			}
			moduleOutputs[callName][callKey][subj.Name] = val
			if isSelf {
				self = val
			}

		case addrs.InputVariable:
			val, valDiags := normalizeRefValue(s.Data.GetInputVariable(subj, rng))
			diags = diags.Append(valDiags)
			inputVariables[subj.Name] = val
			if isSelf {
				self = val
			}

		case addrs.LocalValue:
			val, valDiags := normalizeRefValue(s.Data.GetLocalValue(subj, rng))
			diags = diags.Append(valDiags)
			localValues[subj.Name] = val
			if isSelf {
				self = val
			}

		case addrs.PathAttr:
			val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, rng))
			diags = diags.Append(valDiags)
			pathAttrs[subj.Name] = val
			if isSelf {
				self = val
			}

		case addrs.TerraformAttr:
			val, valDiags := normalizeRefValue(s.Data.GetTerraformAttr(subj, rng))
			diags = diags.Append(valDiags)
			terraformAttrs[subj.Name] = val
			if isSelf {
				self = val
			}

		case addrs.CountAttr:
			val, valDiags := normalizeRefValue(s.Data.GetCountAttr(subj, rng))
			diags = diags.Append(valDiags)
			countAttrs[subj.Name] = val
			if isSelf {
				self = val
			}

		default:
			// Should never happen
			panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))
		}
	}

	for k, v := range buildResourceObjects(managedResources) {
		vals[k] = v
	}
	vals["data"] = cty.ObjectVal(buildResourceObjects(dataResources))
	vals["module"] = cty.ObjectVal(buildModuleObjects(wholeModules, moduleOutputs))
	vals["var"] = cty.ObjectVal(inputVariables)
	vals["local"] = cty.ObjectVal(localValues)
	vals["path"] = cty.ObjectVal(pathAttrs)
	vals["terraform"] = cty.ObjectVal(terraformAttrs)
	vals["count"] = cty.ObjectVal(countAttrs)
	if self != cty.NilVal {
		vals["self"] = self
	}

	return ctx, diags
}

func buildResourceObjects(resources map[string]map[string]map[addrs.InstanceKey]cty.Value) map[string]cty.Value {
	vals := make(map[string]cty.Value)
	for typeName, names := range resources {
		nameVals := make(map[string]cty.Value)
		for name, keys := range names {
			nameVals[name] = buildInstanceObjects(keys)
		}
		vals[typeName] = cty.ObjectVal(nameVals)
	}
	return vals
}

func buildModuleObjects(wholeModules map[string]map[addrs.InstanceKey]cty.Value, moduleOutputs map[string]map[addrs.InstanceKey]map[string]cty.Value) map[string]cty.Value {
	vals := make(map[string]cty.Value)

	for name, keys := range wholeModules {
		vals[name] = buildInstanceObjects(keys)
	}

	for name, keys := range moduleOutputs {
		if _, exists := wholeModules[name]; exists {
			// If we also have a whole module value for this name then we'll
			// skip this since the individual outputs are embedded in that result.
			continue
		}

		// The shape of this collection isn't compatible with buildInstanceObjects,
		// but rather than replicating most of the buildInstanceObjects logic
		// here we'll instead first transform the structure to be what that
		// function expects and then use it. This is a little wasteful, but
		// we do not expect this these maps to be large and so the extra work
		// here should not hurt too much.
		flattened := make(map[addrs.InstanceKey]cty.Value, len(keys))
		for k, vals := range keys {
			flattened[k] = cty.ObjectVal(vals)
		}
		vals[name] = buildInstanceObjects(flattened)
	}

	return vals
}

func buildInstanceObjects(keys map[addrs.InstanceKey]cty.Value) cty.Value {
	if val, exists := keys[addrs.NoKey]; exists {
		// If present, a "no key" value supersedes all other values,
		// since they should be embedded inside it.
		return val
	}

	// If we only have individual values then we need to construct
	// either a list or a map, depending on what sort of keys we
	// have.
	haveInt := false
	haveString := false
	maxInt := 0

	for k := range keys {
		switch tk := k.(type) {
		case addrs.IntKey:
			haveInt = true
			if int(tk) > maxInt {
				maxInt = int(tk)
			}
		case addrs.StringKey:
			haveString = true
		}
	}

	// We should either have ints or strings and not both, but
	// if we have both then we'll prefer strings and let the
	// language interpreter try to convert the int keys into
	// strings in a map.
	switch {
	case haveString:
		vals := make(map[string]cty.Value)
		for k, v := range keys {
			switch tk := k.(type) {
			case addrs.StringKey:
				vals[string(tk)] = v
			case addrs.IntKey:
				sk := strconv.Itoa(int(tk))
				vals[sk] = v
			}
		}
		return cty.ObjectVal(vals)
	case haveInt:
		// We'll make a tuple that is long enough for our maximum
		// index value. It doesn't matter if we end up shorter than
		// the number of instances because if length(...) were
		// being evaluated we would've got a NoKey reference and
		// thus not ended up in this codepath at all.
		vals := make([]cty.Value, maxInt+1)
		for i := range vals {
			if v, exists := keys[addrs.IntKey(i)]; exists {
				vals[i] = v
			} else {
				// Just a placeholder, since nothing will access this anyway
				vals[i] = cty.DynamicVal
			}
		}
		return cty.TupleVal(vals)
	default:
		// Should never happen because there are no other key types.
		log.Printf("[ERROR] strange makeInstanceObjects call with no supported key types")
		return cty.EmptyObjectVal
	}
}

func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) {
	if diags.HasErrors() {
		// If there are errors then we will force an unknown result so that
		// we can still evaluate and catch type errors but we'll avoid
		// producing redundant re-statements of the same errors we've already
		// dealt with here.
		return cty.UnknownVal(val.Type()), diags
	}
	return val, diags
}