1package objchange
2
3import (
4	"fmt"
5
6	"github.com/zclconf/go-cty/cty"
7
8	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
9)
10
11// ProposedNewObject 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 ProposedNewObject(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		prior = AllAttributesNull(schema)
41	}
42	return proposedNewObject(schema, prior, config)
43}
44
45// PlannedDataResourceObject is similar to ProposedNewObject but tailored for
46// planning data resources in particular. Specifically, it replaces the values
47// of any Computed attributes not set in the configuration with an unknown
48// value, which serves as a placeholder for a value to be filled in by the
49// provider when the data resource is finally read.
50//
51// Data resources are different because the planning of them is handled
52// entirely within Terraform Core and not subject to customization by the
53// provider. This function is, in effect, producing an equivalent result to
54// passing the ProposedNewObject result into a provider's PlanResourceChange
55// function, assuming a fixed implementation of PlanResourceChange that just
56// fills in unknown values as needed.
57func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty.Value {
58	// Our trick here is to run the ProposedNewObject logic with an
59	// entirely-unknown prior value. Because of cty's unknown short-circuit
60	// behavior, any operation on prior returns another unknown, and so
61	// unknown values propagate into all of the parts of the resulting value
62	// that would normally be filled in by preserving the prior state.
63	prior := cty.UnknownVal(schema.ImpliedType())
64	return proposedNewObject(schema, prior, config)
65}
66
67func proposedNewObject(schema *configschema.Block, prior, config cty.Value) cty.Value {
68	if config.IsNull() || !config.IsKnown() {
69		// This is a weird situation, but we'll allow it anyway to free
70		// callers from needing to specifically check for these cases.
71		return prior
72	}
73	if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
74		panic("ProposedNewObject only supports object-typed values")
75	}
76
77	// From this point onwards, we can assume that both values are non-null
78	// object types, and that the config value itself is known (though it
79	// may contain nested values that are unknown.)
80
81	newAttrs := map[string]cty.Value{}
82	for name, attr := range schema.Attributes {
83		priorV := prior.GetAttr(name)
84		configV := config.GetAttr(name)
85		var newV cty.Value
86		switch {
87		case attr.Computed && attr.Optional:
88			// This is the trickiest scenario: we want to keep the prior value
89			// if the config isn't overriding it. Note that due to some
90			// ambiguity here, setting an optional+computed attribute from
91			// config and then later switching the config to null in a
92			// subsequent change causes the initial config value to be "sticky"
93			// unless the provider specifically overrides it during its own
94			// plan customization step.
95			if configV.IsNull() {
96				newV = priorV
97			} else {
98				newV = configV
99			}
100		case attr.Computed:
101			// configV will always be null in this case, by definition.
102			// priorV may also be null, but that's okay.
103			newV = priorV
104		default:
105			// For non-computed attributes, we always take the config value,
106			// even if it is null. If it's _required_ then null values
107			// should've been caught during an earlier validation step, and
108			// so we don't really care about that here.
109			newV = configV
110		}
111		newAttrs[name] = newV
112	}
113
114	// Merging nested blocks is a little more complex, since we need to
115	// correlate blocks between both objects and then recursively propose
116	// a new object for each. The correlation logic depends on the nesting
117	// mode for each block type.
118	for name, blockType := range schema.BlockTypes {
119		priorV := prior.GetAttr(name)
120		configV := config.GetAttr(name)
121		var newV cty.Value
122		switch blockType.Nesting {
123
124		case configschema.NestingSingle, configschema.NestingGroup:
125			newV = ProposedNewObject(&blockType.Block, priorV, configV)
126
127		case configschema.NestingList:
128			// Nested blocks are correlated by index.
129			configVLen := 0
130			if configV.IsKnown() && !configV.IsNull() {
131				configVLen = configV.LengthInt()
132			}
133			if configVLen > 0 {
134				newVals := make([]cty.Value, 0, configVLen)
135				for it := configV.ElementIterator(); it.Next(); {
136					idx, configEV := it.Element()
137					if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
138						// If there is no corresponding prior element then
139						// we just take the config value as-is.
140						newVals = append(newVals, configEV)
141						continue
142					}
143					priorEV := priorV.Index(idx)
144
145					newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
146					newVals = append(newVals, newEV)
147				}
148				// Despite the name, a NestingList might also be a tuple, if
149				// its nested schema contains dynamically-typed attributes.
150				if configV.Type().IsTupleType() {
151					newV = cty.TupleVal(newVals)
152				} else {
153					newV = cty.ListVal(newVals)
154				}
155			} else {
156				// Despite the name, a NestingList might also be a tuple, if
157				// its nested schema contains dynamically-typed attributes.
158				if configV.Type().IsTupleType() {
159					newV = cty.EmptyTupleVal
160				} else {
161					newV = cty.ListValEmpty(blockType.ImpliedType())
162				}
163			}
164
165		case configschema.NestingMap:
166			// Despite the name, a NestingMap may produce either a map or
167			// object value, depending on whether the nested schema contains
168			// dynamically-typed attributes.
169			if configV.Type().IsObjectType() {
170				// Nested blocks are correlated by key.
171				configVLen := 0
172				if configV.IsKnown() && !configV.IsNull() {
173					configVLen = configV.LengthInt()
174				}
175				if configVLen > 0 {
176					newVals := make(map[string]cty.Value, configVLen)
177					atys := configV.Type().AttributeTypes()
178					for name := range atys {
179						configEV := configV.GetAttr(name)
180						if !priorV.IsKnown() || priorV.IsNull() || !priorV.Type().HasAttribute(name) {
181							// If there is no corresponding prior element then
182							// we just take the config value as-is.
183							newVals[name] = configEV
184							continue
185						}
186						priorEV := priorV.GetAttr(name)
187
188						newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
189						newVals[name] = newEV
190					}
191					// Although we call the nesting mode "map", we actually use
192					// object values so that elements might have different types
193					// in case of dynamically-typed attributes.
194					newV = cty.ObjectVal(newVals)
195				} else {
196					newV = cty.EmptyObjectVal
197				}
198			} else {
199				configVLen := 0
200				if configV.IsKnown() && !configV.IsNull() {
201					configVLen = configV.LengthInt()
202				}
203				if configVLen > 0 {
204					newVals := make(map[string]cty.Value, configVLen)
205					for it := configV.ElementIterator(); it.Next(); {
206						idx, configEV := it.Element()
207						k := idx.AsString()
208						if priorV.IsKnown() && (priorV.IsNull() || !priorV.HasIndex(idx).True()) {
209							// If there is no corresponding prior element then
210							// we just take the config value as-is.
211							newVals[k] = configEV
212							continue
213						}
214						priorEV := priorV.Index(idx)
215
216						newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
217						newVals[k] = newEV
218					}
219					newV = cty.MapVal(newVals)
220				} else {
221					newV = cty.MapValEmpty(blockType.ImpliedType())
222				}
223			}
224
225		case configschema.NestingSet:
226			if !configV.Type().IsSetType() {
227				panic("configschema.NestingSet value is not a set as expected")
228			}
229
230			// Nested blocks are correlated by comparing the element values
231			// after eliminating all of the computed attributes. In practice,
232			// this means that any config change produces an entirely new
233			// nested object, and we only propagate prior computed values
234			// if the non-computed attribute values are identical.
235			var cmpVals [][2]cty.Value
236			if priorV.IsKnown() && !priorV.IsNull() {
237				cmpVals = setElementCompareValues(&blockType.Block, priorV, false)
238			}
239			configVLen := 0
240			if configV.IsKnown() && !configV.IsNull() {
241				configVLen = configV.LengthInt()
242			}
243			if configVLen > 0 {
244				used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value
245				newVals := make([]cty.Value, 0, configVLen)
246				for it := configV.ElementIterator(); it.Next(); {
247					_, configEV := it.Element()
248					var priorEV cty.Value
249					for i, cmp := range cmpVals {
250						if used[i] {
251							continue
252						}
253						if cmp[1].RawEquals(configEV) {
254							priorEV = cmp[0]
255							used[i] = true // we can't use this value on a future iteration
256							break
257						}
258					}
259					if priorEV == cty.NilVal {
260						priorEV = cty.NullVal(blockType.ImpliedType())
261					}
262
263					newEV := ProposedNewObject(&blockType.Block, priorEV, configEV)
264					newVals = append(newVals, newEV)
265				}
266				newV = cty.SetVal(newVals)
267			} else {
268				newV = cty.SetValEmpty(blockType.Block.ImpliedType())
269			}
270
271		default:
272			// Should never happen, since the above cases are comprehensive.
273			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
274		}
275
276		newAttrs[name] = newV
277	}
278
279	return cty.ObjectVal(newAttrs)
280}
281
282// setElementCompareValues takes a known, non-null value of a cty.Set type and
283// returns a table -- constructed of two-element arrays -- that maps original
284// set element values to corresponding values that have all of the computed
285// values removed, making them suitable for comparison with values obtained
286// from configuration. The element type of the set must conform to the implied
287// type of the given schema, or this function will panic.
288//
289// In the resulting slice, the zeroth element of each array is the original
290// value and the one-indexed element is the corresponding "compare value".
291//
292// This is intended to help correlate prior elements with configured elements
293// in ProposedNewObject. The result is a heuristic rather than an exact science,
294// since e.g. two separate elements may reduce to the same value through this
295// process. The caller must therefore be ready to deal with duplicates.
296func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value {
297	ret := make([][2]cty.Value, 0, set.LengthInt())
298	for it := set.ElementIterator(); it.Next(); {
299		_, ev := it.Element()
300		ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)})
301	}
302	return ret
303}
304
305// setElementCompareValue creates a new value that has all of the same
306// non-computed attribute values as the one given but has all computed
307// attribute values forced to null.
308//
309// If isConfig is true then non-null Optional+Computed attribute values will
310// be preserved. Otherwise, they will also be set to null.
311//
312// The input value must conform to the schema's implied type, and the return
313// value is guaranteed to conform to it.
314func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value {
315	if v.IsNull() || !v.IsKnown() {
316		return v
317	}
318
319	attrs := map[string]cty.Value{}
320	for name, attr := range schema.Attributes {
321		switch {
322		case attr.Computed && attr.Optional:
323			if isConfig {
324				attrs[name] = v.GetAttr(name)
325			} else {
326				attrs[name] = cty.NullVal(attr.Type)
327			}
328		case attr.Computed:
329			attrs[name] = cty.NullVal(attr.Type)
330		default:
331			attrs[name] = v.GetAttr(name)
332		}
333	}
334
335	for name, blockType := range schema.BlockTypes {
336		switch blockType.Nesting {
337
338		case configschema.NestingSingle, configschema.NestingGroup:
339			attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig)
340
341		case configschema.NestingList, configschema.NestingSet:
342			cv := v.GetAttr(name)
343			if cv.IsNull() || !cv.IsKnown() {
344				attrs[name] = cv
345				continue
346			}
347			if l := cv.LengthInt(); l > 0 {
348				elems := make([]cty.Value, 0, l)
349				for it := cv.ElementIterator(); it.Next(); {
350					_, ev := it.Element()
351					elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig))
352				}
353				if blockType.Nesting == configschema.NestingSet {
354					// SetValEmpty would panic if given elements that are not
355					// all of the same type, but that's guaranteed not to
356					// happen here because our input value was _already_ a
357					// set and we've not changed the types of any elements here.
358					attrs[name] = cty.SetVal(elems)
359				} else {
360					attrs[name] = cty.TupleVal(elems)
361				}
362			} else {
363				if blockType.Nesting == configschema.NestingSet {
364					attrs[name] = cty.SetValEmpty(blockType.Block.ImpliedType())
365				} else {
366					attrs[name] = cty.EmptyTupleVal
367				}
368			}
369
370		case configschema.NestingMap:
371			cv := v.GetAttr(name)
372			if cv.IsNull() || !cv.IsKnown() {
373				attrs[name] = cv
374				continue
375			}
376			elems := make(map[string]cty.Value)
377			for it := cv.ElementIterator(); it.Next(); {
378				kv, ev := it.Element()
379				elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig)
380			}
381			attrs[name] = cty.ObjectVal(elems)
382
383		default:
384			// Should never happen, since the above cases are comprehensive.
385			panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting))
386		}
387	}
388
389	return cty.ObjectVal(attrs)
390}
391