1package objchange
2
3import (
4	"fmt"
5
6	"github.com/zclconf/go-cty/cty"
7
8	"github.com/hashicorp/terraform/internal/configs/configschema"
9)
10
11// ProposedNew constructs a proposed new object value by combining the
12// computed attribute values from "prior" with the configured attribute values
13// from "config".
14//
15// Both value must conform to the given schema's implied type, or this function
16// will panic.
17//
18// The prior value must be wholly known, but the config value may be unknown
19// or have nested unknown values.
20//
21// The merging of the two objects includes the attributes of any nested blocks,
22// which will be correlated in a manner appropriate for their nesting mode.
23// Note in particular that the correlation for blocks backed by sets is a
24// heuristic based on matching non-computed attribute values and so it may
25// produce strange results with more "extreme" cases, such as a nested set
26// block where _all_ attributes are computed.
27func ProposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
28	// If the config and prior are both null, return early here before
29	// populating the prior block. The prevents non-null blocks from appearing
30	// the proposed state value.
31	if config.IsNull() && prior.IsNull() {
32		return prior
33	}
34
35	if prior.IsNull() {
36		// In this case, we will construct a synthetic prior value that is
37		// similar to the result of decoding an empty configuration block,
38		// which simplifies our handling of the top-level attributes/blocks
39		// below by giving us one non-null level of object to pull values from.
40		//
41		// "All attributes null" happens to be the definition of EmptyValue for
42		// a Block, so we can just delegate to that
43		prior = schema.EmptyValue()
44	}
45	return proposedNew(schema, prior, config)
46}
47
48// PlannedDataResourceObject is similar to proposedNewBlock but tailored for
49// planning data resources in particular. Specifically, it replaces the values
50// of any Computed attributes not set in the configuration with an unknown
51// value, which serves as a placeholder for a value to be filled in by the
52// provider when the data resource is finally read.
53//
54// Data resources are different because the planning of them is handled
55// entirely within Terraform Core and not subject to customization by the
56// provider. This function is, in effect, producing an equivalent result to
57// passing the proposedNewBlock result into a provider's PlanResourceChange
58// function, assuming a fixed implementation of PlanResourceChange that just
59// fills in unknown values as needed.
60func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
61	// Our trick here is to run the proposedNewBlock logic with an
62	// entirely-unknown prior value. Because of cty's unknown short-circuit
63	// behavior, any operation on prior returns another unknown, and so
64	// unknown values propagate into all of the parts of the resulting value
65	// that would normally be filled in by preserving the prior state.
66	prior := cty.UnknownVal(schema.ImpliedType())
67	return proposedNew(schema, prior, config)
68}
69
70func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
71	if config.IsNull() || !config.IsKnown() {
72		// This is a weird situation, but we'll allow it anyway to free
73		// callers from needing to specifically check for these cases.
74		return prior
75	}
76	if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
77		panic("ProposedNew only supports object-typed values")
78	}
79
80	// From this point onwards, we can assume that both values are non-null
81	// object types, and that the config value itself is known (though it
82	// may contain nested values that are unknown.)
83	newAttrs := proposedNewAttributes(schema.Attributes, prior, config)
84
85	// Merging nested blocks is a little more complex, since we need to
86	// correlate blocks between both objects and then recursively propose
87	// a new object for each. The correlation logic depends on the nesting
88	// mode for each block type.
89	for name, blockType := range schema.BlockTypes {
90		priorV := prior.GetAttr(name)
91		configV := config.GetAttr(name)
92		newAttrs[name] = proposedNewNestedBlock(blockType, priorV, configV)
93	}
94
95	return cty.ObjectVal(newAttrs)
96}
97
98func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value {
99	// The only time we should encounter an entirely unknown block is from the
100	// use of dynamic with an unknown for_each expression.
101	if !config.IsKnown() {
102		return config
103	}
104
105	var newV cty.Value
106
107	switch schema.Nesting {
108
109	case configschema.NestingSingle, configschema.NestingGroup:
110		newV = ProposedNew(&schema.Block, prior, config)
111
112	case configschema.NestingList:
113		// Nested blocks are correlated by index.
114		configVLen := 0
115		if !config.IsNull() {
116			configVLen = config.LengthInt()
117		}
118		if configVLen > 0 {
119			newVals := make([]cty.Value, 0, configVLen)
120			for it := config.ElementIterator(); it.Next(); {
121				idx, configEV := it.Element()
122				if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
123					// If there is no corresponding prior element then
124					// we just take the config value as-is.
125					newVals = append(newVals, configEV)
126					continue
127				}
128				priorEV := prior.Index(idx)
129
130				newEV := ProposedNew(&schema.Block, priorEV, configEV)
131				newVals = append(newVals, newEV)
132			}
133			// Despite the name, a NestingList might also be a tuple, if
134			// its nested schema contains dynamically-typed attributes.
135			if config.Type().IsTupleType() {
136				newV = cty.TupleVal(newVals)
137			} else {
138				newV = cty.ListVal(newVals)
139			}
140		} else {
141			// Despite the name, a NestingList might also be a tuple, if
142			// its nested schema contains dynamically-typed attributes.
143			if config.Type().IsTupleType() {
144				newV = cty.EmptyTupleVal
145			} else {
146				newV = cty.ListValEmpty(schema.ImpliedType())
147			}
148		}
149
150	case configschema.NestingMap:
151		// Despite the name, a NestingMap may produce either a map or
152		// object value, depending on whether the nested schema contains
153		// dynamically-typed attributes.
154		if config.Type().IsObjectType() {
155			// Nested blocks are correlated by key.
156			configVLen := 0
157			if config.IsKnown() && !config.IsNull() {
158				configVLen = config.LengthInt()
159			}
160			if configVLen > 0 {
161				newVals := make(map[string]cty.Value, configVLen)
162				atys := config.Type().AttributeTypes()
163				for name := range atys {
164					configEV := config.GetAttr(name)
165					if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
166						// If there is no corresponding prior element then
167						// we just take the config value as-is.
168						newVals[name] = configEV
169						continue
170					}
171					priorEV := prior.GetAttr(name)
172
173					newEV := ProposedNew(&schema.Block, priorEV, configEV)
174					newVals[name] = newEV
175				}
176				// Although we call the nesting mode "map", we actually use
177				// object values so that elements might have different types
178				// in case of dynamically-typed attributes.
179				newV = cty.ObjectVal(newVals)
180			} else {
181				newV = cty.EmptyObjectVal
182			}
183		} else {
184			configVLen := 0
185			if config.IsKnown() && !config.IsNull() {
186				configVLen = config.LengthInt()
187			}
188			if configVLen > 0 {
189				newVals := make(map[string]cty.Value, configVLen)
190				for it := config.ElementIterator(); it.Next(); {
191					idx, configEV := it.Element()
192					k := idx.AsString()
193					if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
194						// If there is no corresponding prior element then
195						// we just take the config value as-is.
196						newVals[k] = configEV
197						continue
198					}
199					priorEV := prior.Index(idx)
200
201					newEV := ProposedNew(&schema.Block, priorEV, configEV)
202					newVals[k] = newEV
203				}
204				newV = cty.MapVal(newVals)
205			} else {
206				newV = cty.MapValEmpty(schema.ImpliedType())
207			}
208		}
209
210	case configschema.NestingSet:
211		if !config.Type().IsSetType() {
212			panic("configschema.NestingSet value is not a set as expected")
213		}
214
215		// Nested blocks are correlated by comparing the element values
216		// after eliminating all of the computed attributes. In practice,
217		// this means that any config change produces an entirely new
218		// nested object, and we only propagate prior computed values
219		// if the non-computed attribute values are identical.
220		var cmpVals [][2]cty.Value
221		if prior.IsKnown() && !prior.IsNull() {
222			cmpVals = setElementCompareValues(&schema.Block, prior, false)
223		}
224		configVLen := 0
225		if config.IsKnown() && !config.IsNull() {
226			configVLen = config.LengthInt()
227		}
228		if configVLen > 0 {
229			used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
230			newVals := make([]cty.Value, 0, configVLen)
231			for it := config.ElementIterator(); it.Next(); {
232				_, configEV := it.Element()
233				var priorEV cty.Value
234				for i, cmp := range cmpVals {
235					if used[i] {
236						continue
237					}
238					if cmp[1].RawEquals(configEV) {
239						priorEV = cmp[0]
240						used[i] = true // we can't use this value on a future iteration
241						break
242					}
243				}
244				if priorEV == cty.NilVal {
245					priorEV = cty.NullVal(schema.ImpliedType())
246				}
247
248				newEV := ProposedNew(&schema.Block, priorEV, configEV)
249				newVals = append(newVals, newEV)
250			}
251			newV = cty.SetVal(newVals)
252		} else {
253			newV = cty.SetValEmpty(schema.Block.ImpliedType())
254		}
255
256	default:
257		// Should never happen, since the above cases are comprehensive.
258		panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting))
259	}
260	return newV
261}
262
263func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value {
264	newAttrs := make(map[string]cty.Value, len(attrs))
265	for name, attr := range attrs {
266		var priorV cty.Value
267		if prior.IsNull() {
268			priorV = cty.NullVal(prior.Type().AttributeType(name))
269		} else {
270			priorV = prior.GetAttr(name)
271		}
272
273		configV := config.GetAttr(name)
274		var newV cty.Value
275		switch {
276		case attr.Computed && attr.Optional:
277			// This is the trickiest scenario: we want to keep the prior value
278			// if the config isn't overriding it. Note that due to some
279			// ambiguity here, setting an optional+computed attribute from
280			// config and then later switching the config to null in a
281			// subsequent change causes the initial config value to be "sticky"
282			// unless the provider specifically overrides it during its own
283			// plan customization step.
284			if configV.IsNull() {
285				newV = priorV
286			} else {
287				newV = configV
288			}
289		case attr.Computed:
290			// configV will always be null in this case, by definition.
291			// priorV may also be null, but that's okay.
292			newV = priorV
293		default:
294			if attr.NestedType != nil {
295				// For non-computed NestedType attributes, we need to descend
296				// into the individual nested attributes to build the final
297				// value, unless the entire nested attribute is unknown.
298				if !configV.IsKnown() {
299					newV = configV
300				} else {
301					newV = proposedNewNestedType(attr.NestedType, priorV, configV)
302				}
303			} else {
304				// For non-computed attributes, we always take the config value,
305				// even if it is null. If it's _required_ then null values
306				// should've been caught during an earlier validation step, and
307				// so we don't really care about that here.
308				newV = configV
309			}
310		}
311		newAttrs[name] = newV
312	}
313	return newAttrs
314}
315
316func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value {
317	// If the config is null or empty, we will be using this default value.
318	newV := config
319
320	switch schema.Nesting {
321	case configschema.NestingSingle:
322		if !config.IsNull() {
323			newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config))
324		} else {
325			newV = cty.NullVal(config.Type())
326		}
327
328	case configschema.NestingList:
329		// Nested blocks are correlated by index.
330		configVLen := 0
331		if config.IsKnown() && !config.IsNull() {
332			configVLen = config.LengthInt()
333		}
334
335		if configVLen > 0 {
336			newVals := make([]cty.Value, 0, configVLen)
337			for it := config.ElementIterator(); it.Next(); {
338				idx, configEV := it.Element()
339				if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
340					// If there is no corresponding prior element then
341					// we just take the config value as-is.
342					newVals = append(newVals, configEV)
343					continue
344				}
345				priorEV := prior.Index(idx)
346
347				newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
348				newVals = append(newVals, cty.ObjectVal(newEV))
349			}
350			// Despite the name, a NestingList might also be a tuple, if
351			// its nested schema contains dynamically-typed attributes.
352			if config.Type().IsTupleType() {
353				newV = cty.TupleVal(newVals)
354			} else {
355				newV = cty.ListVal(newVals)
356			}
357		}
358
359	case configschema.NestingMap:
360		// Despite the name, a NestingMap may produce either a map or
361		// object value, depending on whether the nested schema contains
362		// dynamically-typed attributes.
363		if config.Type().IsObjectType() {
364			// Nested blocks are correlated by key.
365			configVLen := 0
366			if config.IsKnown() && !config.IsNull() {
367				configVLen = config.LengthInt()
368			}
369			if configVLen > 0 {
370				newVals := make(map[string]cty.Value, configVLen)
371				atys := config.Type().AttributeTypes()
372				for name := range atys {
373					configEV := config.GetAttr(name)
374					if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) {
375						// If there is no corresponding prior element then
376						// we just take the config value as-is.
377						newVals[name] = configEV
378						continue
379					}
380					priorEV := prior.GetAttr(name)
381					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
382					newVals[name] = cty.ObjectVal(newEV)
383				}
384				// Although we call the nesting mode "map", we actually use
385				// object values so that elements might have different types
386				// in case of dynamically-typed attributes.
387				newV = cty.ObjectVal(newVals)
388			}
389		} else {
390			configVLen := 0
391			if config.IsKnown() && !config.IsNull() {
392				configVLen = config.LengthInt()
393			}
394			if configVLen > 0 {
395				newVals := make(map[string]cty.Value, configVLen)
396				for it := config.ElementIterator(); it.Next(); {
397					idx, configEV := it.Element()
398					k := idx.AsString()
399					if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) {
400						// If there is no corresponding prior element then
401						// we just take the config value as-is.
402						newVals[k] = configEV
403						continue
404					}
405					priorEV := prior.Index(idx)
406
407					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
408					newVals[k] = cty.ObjectVal(newEV)
409				}
410				newV = cty.MapVal(newVals)
411			}
412		}
413
414	case configschema.NestingSet:
415		// Nested blocks are correlated by comparing the element values
416		// after eliminating all of the computed attributes. In practice,
417		// this means that any config change produces an entirely new
418		// nested object, and we only propagate prior computed values
419		// if the non-computed attribute values are identical.
420		var cmpVals [][2]cty.Value
421		if prior.IsKnown() && !prior.IsNull() {
422			cmpVals = setElementCompareValuesFromObject(schema, prior)
423		}
424		configVLen := 0
425		if config.IsKnown() && !config.IsNull() {
426			configVLen = config.LengthInt()
427		}
428		if configVLen > 0 {
429			used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
430			newVals := make([]cty.Value, 0, configVLen)
431			for it := config.ElementIterator(); it.Next(); {
432				_, configEV := it.Element()
433				var priorEV cty.Value
434				for i, cmp := range cmpVals {
435					if used[i] {
436						continue
437					}
438					if cmp[1].RawEquals(configEV) {
439						priorEV = cmp[0]
440						used[i] = true // we can't use this value on a future iteration
441						break
442					}
443				}
444				if priorEV == cty.NilVal {
445					newVals = append(newVals, configEV)
446				} else {
447					newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV)
448					newVals = append(newVals, cty.ObjectVal(newEV))
449				}
450			}
451			newV = cty.SetVal(newVals)
452		}
453	}
454
455	return newV
456}
457
458// setElementCompareValues takes a known, non-null value of a cty.Set type and
459// returns a table -- constructed of two-element arrays -- that maps original
460// set element values to corresponding values that have all of the computed
461// values removed, making them suitable for comparison with values obtained
462// from configuration. The element type of the set must conform to the implied
463// type of the given schema, or this function will panic.
464//
465// In the resulting slice, the zeroth element of each array is the original
466// value and the one-indexed element is the corresponding "compare value".
467//
468// This is intended to help correlate prior elements with configured elements
469// in proposedNewBlock. The result is a heuristic rather than an exact science,
470// since e.g. two separate elements may reduce to the same value through this
471// process. The caller must therefore be ready to deal with duplicates.
472func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value {
473	ret := make([][2]cty.Value, 0, set.LengthInt())
474	for it := set.ElementIterator(); it.Next(); {
475		_, ev := it.Element()
476		ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)})
477	}
478	return ret
479}
480
481// setElementCompareValue creates a new value that has all of the same
482// non-computed attribute values as the one given but has all computed
483// attribute values forced to null.
484//
485// If isConfig is true then non-null Optional+Computed attribute values will
486// be preserved. Otherwise, they will also be set to null.
487//
488// The input value must conform to the schema's implied type, and the return
489// value is guaranteed to conform to it.
490func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value {
491	if v.IsNull() || !v.IsKnown() {
492		return v
493	}
494
495	attrs := map[string]cty.Value{}
496	for name, attr := range schema.Attributes {
497		switch {
498		case attr.Computed && attr.Optional:
499			if isConfig {
500				attrs[name] = v.GetAttr(name)
501			} else {
502				attrs[name] = cty.NullVal(attr.Type)
503			}
504		case attr.Computed:
505			attrs[name] = cty.NullVal(attr.Type)
506		default:
507			attrs[name] = v.GetAttr(name)
508		}
509	}
510
511	for name, blockType := range schema.BlockTypes {
512		elementType := blockType.Block.ImpliedType()
513
514		switch blockType.Nesting {
515		case configschema.NestingSingle, configschema.NestingGroup:
516			attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig)
517
518		case configschema.NestingList, configschema.NestingSet:
519			cv := v.GetAttr(name)
520			if cv.IsNull() || !cv.IsKnown() {
521				attrs[name] = cv
522				continue
523			}
524
525			if l := cv.LengthInt(); l > 0 {
526				elems := make([]cty.Value, 0, l)
527				for it := cv.ElementIterator(); it.Next(); {
528					_, ev := it.Element()
529					elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig))
530				}
531
532				switch {
533				case blockType.Nesting == configschema.NestingSet:
534					// SetValEmpty would panic if given elements that are not
535					// all of the same type, but that's guaranteed not to
536					// happen here because our input value was _already_ a
537					// set and we've not changed the types of any elements here.
538					attrs[name] = cty.SetVal(elems)
539
540				// NestingList cases
541				case elementType.HasDynamicTypes():
542					attrs[name] = cty.TupleVal(elems)
543				default:
544					attrs[name] = cty.ListVal(elems)
545				}
546			} else {
547				switch {
548				case blockType.Nesting == configschema.NestingSet:
549					attrs[name] = cty.SetValEmpty(elementType)
550
551				// NestingList cases
552				case elementType.HasDynamicTypes():
553					attrs[name] = cty.EmptyTupleVal
554				default:
555					attrs[name] = cty.ListValEmpty(elementType)
556				}
557			}
558
559		case configschema.NestingMap:
560			cv := v.GetAttr(name)
561			if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 {
562				attrs[name] = cv
563				continue
564			}
565			elems := make(map[string]cty.Value)
566			for it := cv.ElementIterator(); it.Next(); {
567				kv, ev := it.Element()
568				elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig)
569			}
570
571			switch {
572			case elementType.HasDynamicTypes():
573				attrs[name] = cty.ObjectVal(elems)
574			default:
575				attrs[name] = cty.MapVal(elems)
576			}
577
578		default:
579			// Should never happen, since the above cases are comprehensive.
580			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
581		}
582	}
583
584	return cty.ObjectVal(attrs)
585}
586
587// setElementCompareValues takes a known, non-null value of a cty.Set type and
588// returns a table -- constructed of two-element arrays -- that maps original
589// set element values to corresponding values that have all of the computed
590// values removed, making them suitable for comparison with values obtained
591// from configuration. The element type of the set must conform to the implied
592// type of the given schema, or this function will panic.
593//
594// In the resulting slice, the zeroth element of each array is the original
595// value and the one-indexed element is the corresponding "compare value".
596//
597// This is intended to help correlate prior elements with configured elements
598// in proposedNewBlock. The result is a heuristic rather than an exact science,
599// since e.g. two separate elements may reduce to the same value through this
600// process. The caller must therefore be ready to deal with duplicates.
601func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value {
602	ret := make([][2]cty.Value, 0, set.LengthInt())
603	for it := set.ElementIterator(); it.Next(); {
604		_, ev := it.Element()
605		ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)})
606	}
607	return ret
608}
609
610// setElementCompareValue creates a new value that has all of the same
611// non-computed attribute values as the one given but has all computed
612// attribute values forced to null.
613//
614// The input value must conform to the schema's implied type, and the return
615// value is guaranteed to conform to it.
616func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value {
617	if v.IsNull() || !v.IsKnown() {
618		return v
619	}
620	attrs := map[string]cty.Value{}
621
622	for name, attr := range schema.Attributes {
623		attrV := v.GetAttr(name)
624		switch {
625		case attr.Computed:
626			attrs[name] = cty.NullVal(attr.Type)
627		default:
628			attrs[name] = attrV
629		}
630	}
631
632	return cty.ObjectVal(attrs)
633}
634