]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package objchange |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | ||
6 | "github.com/zclconf/go-cty/cty" | |
7 | ||
8 | "github.com/hashicorp/terraform/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. | |
27 | func 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. | |
57 | func 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 | ||
67 | func 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. | |
296 | func 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. | |
314 | func 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 | } |