diff options
author | Nathan Dench <ndenc2@gmail.com> | 2019-05-24 15:16:44 +1000 |
---|---|---|
committer | Nathan Dench <ndenc2@gmail.com> | 2019-05-24 15:16:44 +1000 |
commit | 107c1cdb09c575aa2f61d97f48d8587eb6bada4c (patch) | |
tree | ca7d008643efc555c388baeaf1d986e0b6b3e28c /vendor/github.com/hashicorp/terraform/command/format/diff.go | |
parent | 844b5a68d8af4791755b8f0ad293cc99f5959183 (diff) | |
download | terraform-provider-statuscake-107c1cdb09c575aa2f61d97f48d8587eb6bada4c.tar.gz terraform-provider-statuscake-107c1cdb09c575aa2f61d97f48d8587eb6bada4c.tar.zst terraform-provider-statuscake-107c1cdb09c575aa2f61d97f48d8587eb6bada4c.zip |
Upgrade to 0.12
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/command/format/diff.go')
-rw-r--r-- | vendor/github.com/hashicorp/terraform/command/format/diff.go | 1192 |
1 files changed, 1192 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/command/format/diff.go b/vendor/github.com/hashicorp/terraform/command/format/diff.go new file mode 100644 index 0000000..c726f0e --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/command/format/diff.go | |||
@@ -0,0 +1,1192 @@ | |||
1 | package format | ||
2 | |||
3 | import ( | ||
4 | "bufio" | ||
5 | "bytes" | ||
6 | "fmt" | ||
7 | "sort" | ||
8 | "strings" | ||
9 | |||
10 | "github.com/mitchellh/colorstring" | ||
11 | "github.com/zclconf/go-cty/cty" | ||
12 | ctyjson "github.com/zclconf/go-cty/cty/json" | ||
13 | |||
14 | "github.com/hashicorp/terraform/addrs" | ||
15 | "github.com/hashicorp/terraform/configs/configschema" | ||
16 | "github.com/hashicorp/terraform/plans" | ||
17 | "github.com/hashicorp/terraform/plans/objchange" | ||
18 | "github.com/hashicorp/terraform/states" | ||
19 | ) | ||
20 | |||
21 | // ResourceChange returns a string representation of a change to a particular | ||
22 | // resource, for inclusion in user-facing plan output. | ||
23 | // | ||
24 | // The resource schema must be provided along with the change so that the | ||
25 | // formatted change can reflect the configuration structure for the associated | ||
26 | // resource. | ||
27 | // | ||
28 | // If "color" is non-nil, it will be used to color the result. Otherwise, | ||
29 | // no color codes will be included. | ||
30 | func ResourceChange( | ||
31 | change *plans.ResourceInstanceChangeSrc, | ||
32 | tainted bool, | ||
33 | schema *configschema.Block, | ||
34 | color *colorstring.Colorize, | ||
35 | ) string { | ||
36 | addr := change.Addr | ||
37 | var buf bytes.Buffer | ||
38 | |||
39 | if color == nil { | ||
40 | color = &colorstring.Colorize{ | ||
41 | Colors: colorstring.DefaultColors, | ||
42 | Disable: true, | ||
43 | Reset: false, | ||
44 | } | ||
45 | } | ||
46 | |||
47 | dispAddr := addr.String() | ||
48 | if change.DeposedKey != states.NotDeposed { | ||
49 | dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey) | ||
50 | } | ||
51 | |||
52 | switch change.Action { | ||
53 | case plans.Create: | ||
54 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr))) | ||
55 | case plans.Read: | ||
56 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be read during apply\n # (config refers to values not yet known)", dispAddr))) | ||
57 | case plans.Update: | ||
58 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr))) | ||
59 | case plans.CreateThenDelete, plans.DeleteThenCreate: | ||
60 | if tainted { | ||
61 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr))) | ||
62 | } else { | ||
63 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr))) | ||
64 | } | ||
65 | case plans.Delete: | ||
66 | buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed", dispAddr))) | ||
67 | default: | ||
68 | // should never happen, since the above is exhaustive | ||
69 | buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) | ||
70 | } | ||
71 | buf.WriteString(color.Color("[reset]\n")) | ||
72 | |||
73 | switch change.Action { | ||
74 | case plans.Create: | ||
75 | buf.WriteString(color.Color("[green] +[reset] ")) | ||
76 | case plans.Read: | ||
77 | buf.WriteString(color.Color("[cyan] <=[reset] ")) | ||
78 | case plans.Update: | ||
79 | buf.WriteString(color.Color("[yellow] ~[reset] ")) | ||
80 | case plans.DeleteThenCreate: | ||
81 | buf.WriteString(color.Color("[red]-[reset]/[green]+[reset] ")) | ||
82 | case plans.CreateThenDelete: | ||
83 | buf.WriteString(color.Color("[green]+[reset]/[red]-[reset] ")) | ||
84 | case plans.Delete: | ||
85 | buf.WriteString(color.Color("[red] -[reset] ")) | ||
86 | default: | ||
87 | buf.WriteString(color.Color("??? ")) | ||
88 | } | ||
89 | |||
90 | switch addr.Resource.Resource.Mode { | ||
91 | case addrs.ManagedResourceMode: | ||
92 | buf.WriteString(fmt.Sprintf( | ||
93 | "resource %q %q", | ||
94 | addr.Resource.Resource.Type, | ||
95 | addr.Resource.Resource.Name, | ||
96 | )) | ||
97 | case addrs.DataResourceMode: | ||
98 | buf.WriteString(fmt.Sprintf( | ||
99 | "data %q %q ", | ||
100 | addr.Resource.Resource.Type, | ||
101 | addr.Resource.Resource.Name, | ||
102 | )) | ||
103 | default: | ||
104 | // should never happen, since the above is exhaustive | ||
105 | buf.WriteString(addr.String()) | ||
106 | } | ||
107 | |||
108 | buf.WriteString(" {") | ||
109 | |||
110 | p := blockBodyDiffPrinter{ | ||
111 | buf: &buf, | ||
112 | color: color, | ||
113 | action: change.Action, | ||
114 | requiredReplace: change.RequiredReplace, | ||
115 | } | ||
116 | |||
117 | // Most commonly-used resources have nested blocks that result in us | ||
118 | // going at least three traversals deep while we recurse here, so we'll | ||
119 | // start with that much capacity and then grow as needed for deeper | ||
120 | // structures. | ||
121 | path := make(cty.Path, 0, 3) | ||
122 | |||
123 | changeV, err := change.Decode(schema.ImpliedType()) | ||
124 | if err != nil { | ||
125 | // Should never happen in here, since we've already been through | ||
126 | // loads of layers of encode/decode of the planned changes before now. | ||
127 | panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err)) | ||
128 | } | ||
129 | |||
130 | // We currently have an opt-out that permits the legacy SDK to return values | ||
131 | // that defy our usual conventions around handling of nesting blocks. To | ||
132 | // avoid the rendering code from needing to handle all of these, we'll | ||
133 | // normalize first. | ||
134 | // (Ideally we'd do this as part of the SDK opt-out implementation in core, | ||
135 | // but we've added it here for now to reduce risk of unexpected impacts | ||
136 | // on other code in core.) | ||
137 | changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) | ||
138 | changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) | ||
139 | |||
140 | bodyWritten := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) | ||
141 | if bodyWritten { | ||
142 | buf.WriteString("\n") | ||
143 | buf.WriteString(strings.Repeat(" ", 4)) | ||
144 | } | ||
145 | buf.WriteString("}\n") | ||
146 | |||
147 | return buf.String() | ||
148 | } | ||
149 | |||
150 | type blockBodyDiffPrinter struct { | ||
151 | buf *bytes.Buffer | ||
152 | color *colorstring.Colorize | ||
153 | action plans.Action | ||
154 | requiredReplace cty.PathSet | ||
155 | } | ||
156 | |||
157 | const forcesNewResourceCaption = " [red]# forces replacement[reset]" | ||
158 | |||
159 | // writeBlockBodyDiff writes attribute or block differences | ||
160 | // and returns true if any differences were found and written | ||
161 | func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) bool { | ||
162 | path = ctyEnsurePathCapacity(path, 1) | ||
163 | |||
164 | bodyWritten := false | ||
165 | blankBeforeBlocks := false | ||
166 | { | ||
167 | attrNames := make([]string, 0, len(schema.Attributes)) | ||
168 | attrNameLen := 0 | ||
169 | for name := range schema.Attributes { | ||
170 | oldVal := ctyGetAttrMaybeNull(old, name) | ||
171 | newVal := ctyGetAttrMaybeNull(new, name) | ||
172 | if oldVal.IsNull() && newVal.IsNull() { | ||
173 | // Skip attributes where both old and new values are null | ||
174 | // (we do this early here so that we'll do our value alignment | ||
175 | // based on the longest attribute name that has a change, rather | ||
176 | // than the longest attribute name in the full set.) | ||
177 | continue | ||
178 | } | ||
179 | |||
180 | attrNames = append(attrNames, name) | ||
181 | if len(name) > attrNameLen { | ||
182 | attrNameLen = len(name) | ||
183 | } | ||
184 | } | ||
185 | sort.Strings(attrNames) | ||
186 | if len(attrNames) > 0 { | ||
187 | blankBeforeBlocks = true | ||
188 | } | ||
189 | |||
190 | for _, name := range attrNames { | ||
191 | attrS := schema.Attributes[name] | ||
192 | oldVal := ctyGetAttrMaybeNull(old, name) | ||
193 | newVal := ctyGetAttrMaybeNull(new, name) | ||
194 | |||
195 | bodyWritten = true | ||
196 | p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path) | ||
197 | } | ||
198 | } | ||
199 | |||
200 | { | ||
201 | blockTypeNames := make([]string, 0, len(schema.BlockTypes)) | ||
202 | for name := range schema.BlockTypes { | ||
203 | blockTypeNames = append(blockTypeNames, name) | ||
204 | } | ||
205 | sort.Strings(blockTypeNames) | ||
206 | |||
207 | for _, name := range blockTypeNames { | ||
208 | blockS := schema.BlockTypes[name] | ||
209 | oldVal := ctyGetAttrMaybeNull(old, name) | ||
210 | newVal := ctyGetAttrMaybeNull(new, name) | ||
211 | |||
212 | bodyWritten = true | ||
213 | p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) | ||
214 | |||
215 | // Always include a blank for any subsequent block types. | ||
216 | blankBeforeBlocks = true | ||
217 | } | ||
218 | } | ||
219 | |||
220 | return bodyWritten | ||
221 | } | ||
222 | |||
223 | func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) { | ||
224 | path = append(path, cty.GetAttrStep{Name: name}) | ||
225 | p.buf.WriteString("\n") | ||
226 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
227 | showJustNew := false | ||
228 | var action plans.Action | ||
229 | switch { | ||
230 | case old.IsNull(): | ||
231 | action = plans.Create | ||
232 | showJustNew = true | ||
233 | case new.IsNull(): | ||
234 | action = plans.Delete | ||
235 | case ctyEqualWithUnknown(old, new): | ||
236 | action = plans.NoOp | ||
237 | showJustNew = true | ||
238 | default: | ||
239 | action = plans.Update | ||
240 | } | ||
241 | |||
242 | p.writeActionSymbol(action) | ||
243 | |||
244 | p.buf.WriteString(p.color.Color("[bold]")) | ||
245 | p.buf.WriteString(name) | ||
246 | p.buf.WriteString(p.color.Color("[reset]")) | ||
247 | p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) | ||
248 | p.buf.WriteString(" = ") | ||
249 | |||
250 | if attrS.Sensitive { | ||
251 | p.buf.WriteString("(sensitive value)") | ||
252 | } else { | ||
253 | switch { | ||
254 | case showJustNew: | ||
255 | p.writeValue(new, action, indent+2) | ||
256 | if p.pathForcesNewResource(path) { | ||
257 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
258 | } | ||
259 | default: | ||
260 | // We show new even if it is null to emphasize the fact | ||
261 | // that it is being unset, since otherwise it is easy to | ||
262 | // misunderstand that the value is still set to the old value. | ||
263 | p.writeValueDiff(old, new, indent+2, path) | ||
264 | } | ||
265 | } | ||
266 | } | ||
267 | |||
268 | func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) { | ||
269 | path = append(path, cty.GetAttrStep{Name: name}) | ||
270 | if old.IsNull() && new.IsNull() { | ||
271 | // Nothing to do if both old and new is null | ||
272 | return | ||
273 | } | ||
274 | |||
275 | // Where old/new are collections representing a nesting mode other than | ||
276 | // NestingSingle, we assume the collection value can never be unknown | ||
277 | // since we always produce the container for the nested objects, even if | ||
278 | // the objects within are computed. | ||
279 | |||
280 | switch blockS.Nesting { | ||
281 | case configschema.NestingSingle, configschema.NestingGroup: | ||
282 | var action plans.Action | ||
283 | eqV := new.Equals(old) | ||
284 | switch { | ||
285 | case old.IsNull(): | ||
286 | action = plans.Create | ||
287 | case new.IsNull(): | ||
288 | action = plans.Delete | ||
289 | case !new.IsWhollyKnown() || !old.IsWhollyKnown(): | ||
290 | // "old" should actually always be known due to our contract | ||
291 | // that old values must never be unknown, but we'll allow it | ||
292 | // anyway to be robust. | ||
293 | action = plans.Update | ||
294 | case !eqV.IsKnown() || !eqV.True(): | ||
295 | action = plans.Update | ||
296 | } | ||
297 | |||
298 | if blankBefore { | ||
299 | p.buf.WriteRune('\n') | ||
300 | } | ||
301 | p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path) | ||
302 | case configschema.NestingList: | ||
303 | // For the sake of handling nested blocks, we'll treat a null list | ||
304 | // the same as an empty list since the config language doesn't | ||
305 | // distinguish these anyway. | ||
306 | old = ctyNullBlockListAsEmpty(old) | ||
307 | new = ctyNullBlockListAsEmpty(new) | ||
308 | |||
309 | oldItems := ctyCollectionValues(old) | ||
310 | newItems := ctyCollectionValues(new) | ||
311 | |||
312 | // Here we intentionally preserve the index-based correspondance | ||
313 | // between old and new, rather than trying to detect insertions | ||
314 | // and removals in the list, because this more accurately reflects | ||
315 | // how Terraform Core and providers will understand the change, | ||
316 | // particularly when the nested block contains computed attributes | ||
317 | // that will themselves maintain correspondance by index. | ||
318 | |||
319 | // commonLen is number of elements that exist in both lists, which | ||
320 | // will be presented as updates (~). Any additional items in one | ||
321 | // of the lists will be presented as either creates (+) or deletes (-) | ||
322 | // depending on which list they belong to. | ||
323 | var commonLen int | ||
324 | switch { | ||
325 | case len(oldItems) < len(newItems): | ||
326 | commonLen = len(oldItems) | ||
327 | default: | ||
328 | commonLen = len(newItems) | ||
329 | } | ||
330 | |||
331 | if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) { | ||
332 | p.buf.WriteRune('\n') | ||
333 | } | ||
334 | |||
335 | for i := 0; i < commonLen; i++ { | ||
336 | path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) | ||
337 | oldItem := oldItems[i] | ||
338 | newItem := newItems[i] | ||
339 | action := plans.Update | ||
340 | if oldItem.RawEquals(newItem) { | ||
341 | action = plans.NoOp | ||
342 | } | ||
343 | p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path) | ||
344 | } | ||
345 | for i := commonLen; i < len(oldItems); i++ { | ||
346 | path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) | ||
347 | oldItem := oldItems[i] | ||
348 | newItem := cty.NullVal(oldItem.Type()) | ||
349 | p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path) | ||
350 | } | ||
351 | for i := commonLen; i < len(newItems); i++ { | ||
352 | path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) | ||
353 | newItem := newItems[i] | ||
354 | oldItem := cty.NullVal(newItem.Type()) | ||
355 | p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path) | ||
356 | } | ||
357 | case configschema.NestingSet: | ||
358 | // For the sake of handling nested blocks, we'll treat a null set | ||
359 | // the same as an empty set since the config language doesn't | ||
360 | // distinguish these anyway. | ||
361 | old = ctyNullBlockSetAsEmpty(old) | ||
362 | new = ctyNullBlockSetAsEmpty(new) | ||
363 | |||
364 | oldItems := ctyCollectionValues(old) | ||
365 | newItems := ctyCollectionValues(new) | ||
366 | |||
367 | if (len(oldItems) + len(newItems)) == 0 { | ||
368 | // Nothing to do if both sets are empty | ||
369 | return | ||
370 | } | ||
371 | |||
372 | allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) | ||
373 | allItems = append(allItems, oldItems...) | ||
374 | allItems = append(allItems, newItems...) | ||
375 | all := cty.SetVal(allItems) | ||
376 | |||
377 | if blankBefore { | ||
378 | p.buf.WriteRune('\n') | ||
379 | } | ||
380 | |||
381 | for it := all.ElementIterator(); it.Next(); { | ||
382 | _, val := it.Element() | ||
383 | var action plans.Action | ||
384 | var oldValue, newValue cty.Value | ||
385 | switch { | ||
386 | case !val.IsKnown(): | ||
387 | action = plans.Update | ||
388 | newValue = val | ||
389 | case !old.HasElement(val).True(): | ||
390 | action = plans.Create | ||
391 | oldValue = cty.NullVal(val.Type()) | ||
392 | newValue = val | ||
393 | case !new.HasElement(val).True(): | ||
394 | action = plans.Delete | ||
395 | oldValue = val | ||
396 | newValue = cty.NullVal(val.Type()) | ||
397 | default: | ||
398 | action = plans.NoOp | ||
399 | oldValue = val | ||
400 | newValue = val | ||
401 | } | ||
402 | path := append(path, cty.IndexStep{Key: val}) | ||
403 | p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path) | ||
404 | } | ||
405 | |||
406 | case configschema.NestingMap: | ||
407 | // For the sake of handling nested blocks, we'll treat a null map | ||
408 | // the same as an empty map since the config language doesn't | ||
409 | // distinguish these anyway. | ||
410 | old = ctyNullBlockMapAsEmpty(old) | ||
411 | new = ctyNullBlockMapAsEmpty(new) | ||
412 | |||
413 | oldItems := old.AsValueMap() | ||
414 | newItems := new.AsValueMap() | ||
415 | if (len(oldItems) + len(newItems)) == 0 { | ||
416 | // Nothing to do if both maps are empty | ||
417 | return | ||
418 | } | ||
419 | |||
420 | allKeys := make(map[string]bool) | ||
421 | for k := range oldItems { | ||
422 | allKeys[k] = true | ||
423 | } | ||
424 | for k := range newItems { | ||
425 | allKeys[k] = true | ||
426 | } | ||
427 | allKeysOrder := make([]string, 0, len(allKeys)) | ||
428 | for k := range allKeys { | ||
429 | allKeysOrder = append(allKeysOrder, k) | ||
430 | } | ||
431 | sort.Strings(allKeysOrder) | ||
432 | |||
433 | if blankBefore { | ||
434 | p.buf.WriteRune('\n') | ||
435 | } | ||
436 | |||
437 | for _, k := range allKeysOrder { | ||
438 | var action plans.Action | ||
439 | oldValue := oldItems[k] | ||
440 | newValue := newItems[k] | ||
441 | switch { | ||
442 | case oldValue == cty.NilVal: | ||
443 | oldValue = cty.NullVal(newValue.Type()) | ||
444 | action = plans.Create | ||
445 | case newValue == cty.NilVal: | ||
446 | newValue = cty.NullVal(oldValue.Type()) | ||
447 | action = plans.Delete | ||
448 | case !newValue.RawEquals(oldValue): | ||
449 | action = plans.Update | ||
450 | default: | ||
451 | action = plans.NoOp | ||
452 | } | ||
453 | |||
454 | path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) | ||
455 | p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path) | ||
456 | } | ||
457 | } | ||
458 | } | ||
459 | |||
460 | func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) { | ||
461 | p.buf.WriteString("\n") | ||
462 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
463 | p.writeActionSymbol(action) | ||
464 | |||
465 | if label != nil { | ||
466 | fmt.Fprintf(p.buf, "%s %q {", name, *label) | ||
467 | } else { | ||
468 | fmt.Fprintf(p.buf, "%s {", name) | ||
469 | } | ||
470 | |||
471 | if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { | ||
472 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
473 | } | ||
474 | |||
475 | bodyWritten := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) | ||
476 | if bodyWritten { | ||
477 | p.buf.WriteString("\n") | ||
478 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
479 | } | ||
480 | p.buf.WriteString("}") | ||
481 | } | ||
482 | |||
483 | func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { | ||
484 | if !val.IsKnown() { | ||
485 | p.buf.WriteString("(known after apply)") | ||
486 | return | ||
487 | } | ||
488 | if val.IsNull() { | ||
489 | p.buf.WriteString(p.color.Color("[dark_gray]null[reset]")) | ||
490 | return | ||
491 | } | ||
492 | |||
493 | ty := val.Type() | ||
494 | |||
495 | switch { | ||
496 | case ty.IsPrimitiveType(): | ||
497 | switch ty { | ||
498 | case cty.String: | ||
499 | { | ||
500 | // Special behavior for JSON strings containing array or object | ||
501 | src := []byte(val.AsString()) | ||
502 | ty, err := ctyjson.ImpliedType(src) | ||
503 | // check for the special case of "null", which decodes to nil, | ||
504 | // and just allow it to be printed out directly | ||
505 | if err == nil && !ty.IsPrimitiveType() && val.AsString() != "null" { | ||
506 | jv, err := ctyjson.Unmarshal(src, ty) | ||
507 | if err == nil { | ||
508 | p.buf.WriteString("jsonencode(") | ||
509 | if jv.LengthInt() == 0 { | ||
510 | p.writeValue(jv, action, 0) | ||
511 | } else { | ||
512 | p.buf.WriteByte('\n') | ||
513 | p.buf.WriteString(strings.Repeat(" ", indent+4)) | ||
514 | p.writeValue(jv, action, indent+4) | ||
515 | p.buf.WriteByte('\n') | ||
516 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
517 | } | ||
518 | p.buf.WriteByte(')') | ||
519 | break // don't *also* do the normal behavior below | ||
520 | } | ||
521 | } | ||
522 | } | ||
523 | fmt.Fprintf(p.buf, "%q", val.AsString()) | ||
524 | case cty.Bool: | ||
525 | if val.True() { | ||
526 | p.buf.WriteString("true") | ||
527 | } else { | ||
528 | p.buf.WriteString("false") | ||
529 | } | ||
530 | case cty.Number: | ||
531 | bf := val.AsBigFloat() | ||
532 | p.buf.WriteString(bf.Text('f', -1)) | ||
533 | default: | ||
534 | // should never happen, since the above is exhaustive | ||
535 | fmt.Fprintf(p.buf, "%#v", val) | ||
536 | } | ||
537 | case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): | ||
538 | p.buf.WriteString("[") | ||
539 | |||
540 | it := val.ElementIterator() | ||
541 | for it.Next() { | ||
542 | _, val := it.Element() | ||
543 | |||
544 | p.buf.WriteString("\n") | ||
545 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
546 | p.writeActionSymbol(action) | ||
547 | p.writeValue(val, action, indent+4) | ||
548 | p.buf.WriteString(",") | ||
549 | } | ||
550 | |||
551 | if val.LengthInt() > 0 { | ||
552 | p.buf.WriteString("\n") | ||
553 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
554 | } | ||
555 | p.buf.WriteString("]") | ||
556 | case ty.IsMapType(): | ||
557 | p.buf.WriteString("{") | ||
558 | |||
559 | keyLen := 0 | ||
560 | for it := val.ElementIterator(); it.Next(); { | ||
561 | key, _ := it.Element() | ||
562 | if keyStr := key.AsString(); len(keyStr) > keyLen { | ||
563 | keyLen = len(keyStr) | ||
564 | } | ||
565 | } | ||
566 | |||
567 | for it := val.ElementIterator(); it.Next(); { | ||
568 | key, val := it.Element() | ||
569 | |||
570 | p.buf.WriteString("\n") | ||
571 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
572 | p.writeActionSymbol(action) | ||
573 | p.writeValue(key, action, indent+4) | ||
574 | p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString()))) | ||
575 | p.buf.WriteString(" = ") | ||
576 | p.writeValue(val, action, indent+4) | ||
577 | } | ||
578 | |||
579 | if val.LengthInt() > 0 { | ||
580 | p.buf.WriteString("\n") | ||
581 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
582 | } | ||
583 | p.buf.WriteString("}") | ||
584 | case ty.IsObjectType(): | ||
585 | p.buf.WriteString("{") | ||
586 | |||
587 | atys := ty.AttributeTypes() | ||
588 | attrNames := make([]string, 0, len(atys)) | ||
589 | nameLen := 0 | ||
590 | for attrName := range atys { | ||
591 | attrNames = append(attrNames, attrName) | ||
592 | if len(attrName) > nameLen { | ||
593 | nameLen = len(attrName) | ||
594 | } | ||
595 | } | ||
596 | sort.Strings(attrNames) | ||
597 | |||
598 | for _, attrName := range attrNames { | ||
599 | val := val.GetAttr(attrName) | ||
600 | |||
601 | p.buf.WriteString("\n") | ||
602 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
603 | p.writeActionSymbol(action) | ||
604 | p.buf.WriteString(attrName) | ||
605 | p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName))) | ||
606 | p.buf.WriteString(" = ") | ||
607 | p.writeValue(val, action, indent+4) | ||
608 | } | ||
609 | |||
610 | if len(attrNames) > 0 { | ||
611 | p.buf.WriteString("\n") | ||
612 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
613 | } | ||
614 | p.buf.WriteString("}") | ||
615 | } | ||
616 | } | ||
617 | |||
618 | func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) { | ||
619 | ty := old.Type() | ||
620 | typesEqual := ctyTypesEqual(ty, new.Type()) | ||
621 | |||
622 | // We have some specialized diff implementations for certain complex | ||
623 | // values where it's useful to see a visualization of the diff of | ||
624 | // the nested elements rather than just showing the entire old and | ||
625 | // new values verbatim. | ||
626 | // However, these specialized implementations can apply only if both | ||
627 | // values are known and non-null. | ||
628 | if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual { | ||
629 | switch { | ||
630 | case ty == cty.String: | ||
631 | // We have special behavior for both multi-line strings in general | ||
632 | // and for strings that can parse as JSON. For the JSON handling | ||
633 | // to apply, both old and new must be valid JSON. | ||
634 | // For single-line strings that don't parse as JSON we just fall | ||
635 | // out of this switch block and do the default old -> new rendering. | ||
636 | oldS := old.AsString() | ||
637 | newS := new.AsString() | ||
638 | |||
639 | { | ||
640 | // Special behavior for JSON strings containing object or | ||
641 | // list values. | ||
642 | oldBytes := []byte(oldS) | ||
643 | newBytes := []byte(newS) | ||
644 | oldType, oldErr := ctyjson.ImpliedType(oldBytes) | ||
645 | newType, newErr := ctyjson.ImpliedType(newBytes) | ||
646 | if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) { | ||
647 | oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType) | ||
648 | newJV, newErr := ctyjson.Unmarshal(newBytes, newType) | ||
649 | if oldErr == nil && newErr == nil { | ||
650 | if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace | ||
651 | p.buf.WriteString("jsonencode(") | ||
652 | p.buf.WriteByte('\n') | ||
653 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
654 | p.writeActionSymbol(plans.Update) | ||
655 | p.writeValueDiff(oldJV, newJV, indent+4, path) | ||
656 | p.buf.WriteByte('\n') | ||
657 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
658 | p.buf.WriteByte(')') | ||
659 | } else { | ||
660 | // if they differ only in insigificant whitespace | ||
661 | // then we'll note that but still expand out the | ||
662 | // effective value. | ||
663 | if p.pathForcesNewResource(path) { | ||
664 | p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]")) | ||
665 | } else { | ||
666 | p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]")) | ||
667 | } | ||
668 | p.buf.WriteByte('\n') | ||
669 | p.buf.WriteString(strings.Repeat(" ", indent+4)) | ||
670 | p.writeValue(oldJV, plans.NoOp, indent+4) | ||
671 | p.buf.WriteByte('\n') | ||
672 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
673 | p.buf.WriteByte(')') | ||
674 | } | ||
675 | return | ||
676 | } | ||
677 | } | ||
678 | } | ||
679 | |||
680 | if strings.Index(oldS, "\n") < 0 && strings.Index(newS, "\n") < 0 { | ||
681 | break | ||
682 | } | ||
683 | |||
684 | p.buf.WriteString("<<~EOT") | ||
685 | if p.pathForcesNewResource(path) { | ||
686 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
687 | } | ||
688 | p.buf.WriteString("\n") | ||
689 | |||
690 | var oldLines, newLines []cty.Value | ||
691 | { | ||
692 | r := strings.NewReader(oldS) | ||
693 | sc := bufio.NewScanner(r) | ||
694 | for sc.Scan() { | ||
695 | oldLines = append(oldLines, cty.StringVal(sc.Text())) | ||
696 | } | ||
697 | } | ||
698 | { | ||
699 | r := strings.NewReader(newS) | ||
700 | sc := bufio.NewScanner(r) | ||
701 | for sc.Scan() { | ||
702 | newLines = append(newLines, cty.StringVal(sc.Text())) | ||
703 | } | ||
704 | } | ||
705 | |||
706 | diffLines := ctySequenceDiff(oldLines, newLines) | ||
707 | for _, diffLine := range diffLines { | ||
708 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
709 | p.writeActionSymbol(diffLine.Action) | ||
710 | |||
711 | switch diffLine.Action { | ||
712 | case plans.NoOp, plans.Delete: | ||
713 | p.buf.WriteString(diffLine.Before.AsString()) | ||
714 | case plans.Create: | ||
715 | p.buf.WriteString(diffLine.After.AsString()) | ||
716 | default: | ||
717 | // Should never happen since the above covers all | ||
718 | // actions that ctySequenceDiff can return for strings | ||
719 | p.buf.WriteString(diffLine.After.AsString()) | ||
720 | |||
721 | } | ||
722 | p.buf.WriteString("\n") | ||
723 | } | ||
724 | |||
725 | p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol | ||
726 | p.buf.WriteString("EOT") | ||
727 | |||
728 | return | ||
729 | |||
730 | case ty.IsSetType(): | ||
731 | p.buf.WriteString("[") | ||
732 | if p.pathForcesNewResource(path) { | ||
733 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
734 | } | ||
735 | p.buf.WriteString("\n") | ||
736 | |||
737 | var addedVals, removedVals, allVals []cty.Value | ||
738 | for it := old.ElementIterator(); it.Next(); { | ||
739 | _, val := it.Element() | ||
740 | allVals = append(allVals, val) | ||
741 | if new.HasElement(val).False() { | ||
742 | removedVals = append(removedVals, val) | ||
743 | } | ||
744 | } | ||
745 | for it := new.ElementIterator(); it.Next(); { | ||
746 | _, val := it.Element() | ||
747 | allVals = append(allVals, val) | ||
748 | if val.IsKnown() && old.HasElement(val).False() { | ||
749 | addedVals = append(addedVals, val) | ||
750 | } | ||
751 | } | ||
752 | |||
753 | var all, added, removed cty.Value | ||
754 | if len(allVals) > 0 { | ||
755 | all = cty.SetVal(allVals) | ||
756 | } else { | ||
757 | all = cty.SetValEmpty(ty.ElementType()) | ||
758 | } | ||
759 | if len(addedVals) > 0 { | ||
760 | added = cty.SetVal(addedVals) | ||
761 | } else { | ||
762 | added = cty.SetValEmpty(ty.ElementType()) | ||
763 | } | ||
764 | if len(removedVals) > 0 { | ||
765 | removed = cty.SetVal(removedVals) | ||
766 | } else { | ||
767 | removed = cty.SetValEmpty(ty.ElementType()) | ||
768 | } | ||
769 | |||
770 | for it := all.ElementIterator(); it.Next(); { | ||
771 | _, val := it.Element() | ||
772 | |||
773 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
774 | |||
775 | var action plans.Action | ||
776 | switch { | ||
777 | case !val.IsKnown(): | ||
778 | action = plans.Update | ||
779 | case added.HasElement(val).True(): | ||
780 | action = plans.Create | ||
781 | case removed.HasElement(val).True(): | ||
782 | action = plans.Delete | ||
783 | default: | ||
784 | action = plans.NoOp | ||
785 | } | ||
786 | |||
787 | p.writeActionSymbol(action) | ||
788 | p.writeValue(val, action, indent+4) | ||
789 | p.buf.WriteString(",\n") | ||
790 | } | ||
791 | |||
792 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
793 | p.buf.WriteString("]") | ||
794 | return | ||
795 | case ty.IsListType() || ty.IsTupleType(): | ||
796 | p.buf.WriteString("[") | ||
797 | if p.pathForcesNewResource(path) { | ||
798 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
799 | } | ||
800 | p.buf.WriteString("\n") | ||
801 | |||
802 | elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice()) | ||
803 | for _, elemDiff := range elemDiffs { | ||
804 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
805 | p.writeActionSymbol(elemDiff.Action) | ||
806 | switch elemDiff.Action { | ||
807 | case plans.NoOp, plans.Delete: | ||
808 | p.writeValue(elemDiff.Before, elemDiff.Action, indent+4) | ||
809 | case plans.Update: | ||
810 | p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path) | ||
811 | case plans.Create: | ||
812 | p.writeValue(elemDiff.After, elemDiff.Action, indent+4) | ||
813 | default: | ||
814 | // Should never happen since the above covers all | ||
815 | // actions that ctySequenceDiff can return. | ||
816 | p.writeValue(elemDiff.After, elemDiff.Action, indent+4) | ||
817 | } | ||
818 | |||
819 | p.buf.WriteString(",\n") | ||
820 | } | ||
821 | |||
822 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
823 | p.buf.WriteString("]") | ||
824 | return | ||
825 | |||
826 | case ty.IsMapType(): | ||
827 | p.buf.WriteString("{") | ||
828 | if p.pathForcesNewResource(path) { | ||
829 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
830 | } | ||
831 | p.buf.WriteString("\n") | ||
832 | |||
833 | var allKeys []string | ||
834 | keyLen := 0 | ||
835 | for it := old.ElementIterator(); it.Next(); { | ||
836 | k, _ := it.Element() | ||
837 | keyStr := k.AsString() | ||
838 | allKeys = append(allKeys, keyStr) | ||
839 | if len(keyStr) > keyLen { | ||
840 | keyLen = len(keyStr) | ||
841 | } | ||
842 | } | ||
843 | for it := new.ElementIterator(); it.Next(); { | ||
844 | k, _ := it.Element() | ||
845 | keyStr := k.AsString() | ||
846 | allKeys = append(allKeys, keyStr) | ||
847 | if len(keyStr) > keyLen { | ||
848 | keyLen = len(keyStr) | ||
849 | } | ||
850 | } | ||
851 | |||
852 | sort.Strings(allKeys) | ||
853 | |||
854 | lastK := "" | ||
855 | for i, k := range allKeys { | ||
856 | if i > 0 && lastK == k { | ||
857 | continue // skip duplicates (list is sorted) | ||
858 | } | ||
859 | lastK = k | ||
860 | |||
861 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
862 | kV := cty.StringVal(k) | ||
863 | var action plans.Action | ||
864 | if old.HasIndex(kV).False() { | ||
865 | action = plans.Create | ||
866 | } else if new.HasIndex(kV).False() { | ||
867 | action = plans.Delete | ||
868 | } else if eqV := old.Index(kV).Equals(new.Index(kV)); eqV.IsKnown() && eqV.True() { | ||
869 | action = plans.NoOp | ||
870 | } else { | ||
871 | action = plans.Update | ||
872 | } | ||
873 | |||
874 | path := append(path, cty.IndexStep{Key: kV}) | ||
875 | |||
876 | p.writeActionSymbol(action) | ||
877 | p.writeValue(kV, action, indent+4) | ||
878 | p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) | ||
879 | p.buf.WriteString(" = ") | ||
880 | switch action { | ||
881 | case plans.Create, plans.NoOp: | ||
882 | v := new.Index(kV) | ||
883 | p.writeValue(v, action, indent+4) | ||
884 | case plans.Delete: | ||
885 | oldV := old.Index(kV) | ||
886 | newV := cty.NullVal(oldV.Type()) | ||
887 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
888 | default: | ||
889 | oldV := old.Index(kV) | ||
890 | newV := new.Index(kV) | ||
891 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
892 | } | ||
893 | |||
894 | p.buf.WriteByte('\n') | ||
895 | } | ||
896 | |||
897 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
898 | p.buf.WriteString("}") | ||
899 | return | ||
900 | case ty.IsObjectType(): | ||
901 | p.buf.WriteString("{") | ||
902 | p.buf.WriteString("\n") | ||
903 | |||
904 | forcesNewResource := p.pathForcesNewResource(path) | ||
905 | |||
906 | var allKeys []string | ||
907 | keyLen := 0 | ||
908 | for it := old.ElementIterator(); it.Next(); { | ||
909 | k, _ := it.Element() | ||
910 | keyStr := k.AsString() | ||
911 | allKeys = append(allKeys, keyStr) | ||
912 | if len(keyStr) > keyLen { | ||
913 | keyLen = len(keyStr) | ||
914 | } | ||
915 | } | ||
916 | for it := new.ElementIterator(); it.Next(); { | ||
917 | k, _ := it.Element() | ||
918 | keyStr := k.AsString() | ||
919 | allKeys = append(allKeys, keyStr) | ||
920 | if len(keyStr) > keyLen { | ||
921 | keyLen = len(keyStr) | ||
922 | } | ||
923 | } | ||
924 | |||
925 | sort.Strings(allKeys) | ||
926 | |||
927 | lastK := "" | ||
928 | for i, k := range allKeys { | ||
929 | if i > 0 && lastK == k { | ||
930 | continue // skip duplicates (list is sorted) | ||
931 | } | ||
932 | lastK = k | ||
933 | |||
934 | p.buf.WriteString(strings.Repeat(" ", indent+2)) | ||
935 | kV := k | ||
936 | var action plans.Action | ||
937 | if !old.Type().HasAttribute(kV) { | ||
938 | action = plans.Create | ||
939 | } else if !new.Type().HasAttribute(kV) { | ||
940 | action = plans.Delete | ||
941 | } else if eqV := old.GetAttr(kV).Equals(new.GetAttr(kV)); eqV.IsKnown() && eqV.True() { | ||
942 | action = plans.NoOp | ||
943 | } else { | ||
944 | action = plans.Update | ||
945 | } | ||
946 | |||
947 | path := append(path, cty.GetAttrStep{Name: kV}) | ||
948 | |||
949 | p.writeActionSymbol(action) | ||
950 | p.buf.WriteString(k) | ||
951 | p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) | ||
952 | p.buf.WriteString(" = ") | ||
953 | |||
954 | switch action { | ||
955 | case plans.Create, plans.NoOp: | ||
956 | v := new.GetAttr(kV) | ||
957 | p.writeValue(v, action, indent+4) | ||
958 | case plans.Delete: | ||
959 | oldV := old.GetAttr(kV) | ||
960 | newV := cty.NullVal(oldV.Type()) | ||
961 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
962 | default: | ||
963 | oldV := old.GetAttr(kV) | ||
964 | newV := new.GetAttr(kV) | ||
965 | p.writeValueDiff(oldV, newV, indent+4, path) | ||
966 | } | ||
967 | |||
968 | p.buf.WriteString("\n") | ||
969 | } | ||
970 | |||
971 | p.buf.WriteString(strings.Repeat(" ", indent)) | ||
972 | p.buf.WriteString("}") | ||
973 | |||
974 | if forcesNewResource { | ||
975 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
976 | } | ||
977 | return | ||
978 | } | ||
979 | } | ||
980 | |||
981 | // In all other cases, we just show the new and old values as-is | ||
982 | p.writeValue(old, plans.Delete, indent) | ||
983 | if new.IsNull() { | ||
984 | p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] ")) | ||
985 | } else { | ||
986 | p.buf.WriteString(p.color.Color(" [yellow]->[reset] ")) | ||
987 | } | ||
988 | |||
989 | p.writeValue(new, plans.Create, indent) | ||
990 | if p.pathForcesNewResource(path) { | ||
991 | p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) | ||
992 | } | ||
993 | } | ||
994 | |||
995 | // writeActionSymbol writes a symbol to represent the given action, followed | ||
996 | // by a space. | ||
997 | // | ||
998 | // It only supports the actions that can be represented with a single character: | ||
999 | // Create, Delete, Update and NoAction. | ||
1000 | func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) { | ||
1001 | switch action { | ||
1002 | case plans.Create: | ||
1003 | p.buf.WriteString(p.color.Color("[green]+[reset] ")) | ||
1004 | case plans.Delete: | ||
1005 | p.buf.WriteString(p.color.Color("[red]-[reset] ")) | ||
1006 | case plans.Update: | ||
1007 | p.buf.WriteString(p.color.Color("[yellow]~[reset] ")) | ||
1008 | case plans.NoOp: | ||
1009 | p.buf.WriteString(" ") | ||
1010 | default: | ||
1011 | // Should never happen | ||
1012 | p.buf.WriteString(p.color.Color("? ")) | ||
1013 | } | ||
1014 | } | ||
1015 | |||
1016 | func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool { | ||
1017 | if !p.action.IsReplace() { | ||
1018 | // "requiredReplace" only applies when the instance is being replaced | ||
1019 | return false | ||
1020 | } | ||
1021 | return p.requiredReplace.Has(path) | ||
1022 | } | ||
1023 | |||
1024 | func ctyEmptyString(value cty.Value) bool { | ||
1025 | if !value.IsNull() && value.IsKnown() { | ||
1026 | valueType := value.Type() | ||
1027 | if valueType == cty.String && value.AsString() == "" { | ||
1028 | return true | ||
1029 | } | ||
1030 | } | ||
1031 | return false | ||
1032 | } | ||
1033 | |||
1034 | func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value { | ||
1035 | attrType := val.Type().AttributeType(name) | ||
1036 | |||
1037 | if val.IsNull() { | ||
1038 | return cty.NullVal(attrType) | ||
1039 | } | ||
1040 | |||
1041 | // We treat "" as null here | ||
1042 | // as existing SDK doesn't support null yet. | ||
1043 | // This allows us to avoid spurious diffs | ||
1044 | // until we introduce null to the SDK. | ||
1045 | attrValue := val.GetAttr(name) | ||
1046 | if ctyEmptyString(attrValue) { | ||
1047 | return cty.NullVal(attrType) | ||
1048 | } | ||
1049 | |||
1050 | return attrValue | ||
1051 | } | ||
1052 | |||
1053 | func ctyCollectionValues(val cty.Value) []cty.Value { | ||
1054 | if !val.IsKnown() || val.IsNull() { | ||
1055 | return nil | ||
1056 | } | ||
1057 | |||
1058 | ret := make([]cty.Value, 0, val.LengthInt()) | ||
1059 | for it := val.ElementIterator(); it.Next(); { | ||
1060 | _, value := it.Element() | ||
1061 | ret = append(ret, value) | ||
1062 | } | ||
1063 | return ret | ||
1064 | } | ||
1065 | |||
1066 | // ctySequenceDiff returns differences between given sequences of cty.Value(s) | ||
1067 | // in the form of Create, Delete, or Update actions (for objects). | ||
1068 | func ctySequenceDiff(old, new []cty.Value) []*plans.Change { | ||
1069 | var ret []*plans.Change | ||
1070 | lcs := objchange.LongestCommonSubsequence(old, new) | ||
1071 | var oldI, newI, lcsI int | ||
1072 | for oldI < len(old) || newI < len(new) || lcsI < len(lcs) { | ||
1073 | for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) { | ||
1074 | isObjectDiff := old[oldI].Type().IsObjectType() && (newI >= len(new) || new[newI].Type().IsObjectType()) | ||
1075 | if isObjectDiff && newI < len(new) { | ||
1076 | ret = append(ret, &plans.Change{ | ||
1077 | Action: plans.Update, | ||
1078 | Before: old[oldI], | ||
1079 | After: new[newI], | ||
1080 | }) | ||
1081 | oldI++ | ||
1082 | newI++ // we also consume the next "new" in this case | ||
1083 | continue | ||
1084 | } | ||
1085 | |||
1086 | ret = append(ret, &plans.Change{ | ||
1087 | Action: plans.Delete, | ||
1088 | Before: old[oldI], | ||
1089 | After: cty.NullVal(old[oldI].Type()), | ||
1090 | }) | ||
1091 | oldI++ | ||
1092 | } | ||
1093 | for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) { | ||
1094 | ret = append(ret, &plans.Change{ | ||
1095 | Action: plans.Create, | ||
1096 | Before: cty.NullVal(new[newI].Type()), | ||
1097 | After: new[newI], | ||
1098 | }) | ||
1099 | newI++ | ||
1100 | } | ||
1101 | if lcsI < len(lcs) { | ||
1102 | ret = append(ret, &plans.Change{ | ||
1103 | Action: plans.NoOp, | ||
1104 | Before: lcs[lcsI], | ||
1105 | After: lcs[lcsI], | ||
1106 | }) | ||
1107 | |||
1108 | // All of our indexes advance together now, since the line | ||
1109 | // is common to all three sequences. | ||
1110 | lcsI++ | ||
1111 | oldI++ | ||
1112 | newI++ | ||
1113 | } | ||
1114 | } | ||
1115 | return ret | ||
1116 | } | ||
1117 | |||
1118 | func ctyEqualWithUnknown(old, new cty.Value) bool { | ||
1119 | if !old.IsWhollyKnown() || !new.IsWhollyKnown() { | ||
1120 | return false | ||
1121 | } | ||
1122 | return old.Equals(new).True() | ||
1123 | } | ||
1124 | |||
1125 | // ctyTypesEqual checks equality of two types more loosely | ||
1126 | // by avoiding checks of object/tuple elements | ||
1127 | // as we render differences on element-by-element basis anyway | ||
1128 | func ctyTypesEqual(oldT, newT cty.Type) bool { | ||
1129 | if oldT.IsObjectType() && newT.IsObjectType() { | ||
1130 | return true | ||
1131 | } | ||
1132 | if oldT.IsTupleType() && newT.IsTupleType() { | ||
1133 | return true | ||
1134 | } | ||
1135 | return oldT.Equals(newT) | ||
1136 | } | ||
1137 | |||
1138 | func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path { | ||
1139 | if cap(path)-len(path) >= minExtra { | ||
1140 | return path | ||
1141 | } | ||
1142 | newCap := cap(path) * 2 | ||
1143 | if newCap < (len(path) + minExtra) { | ||
1144 | newCap = len(path) + minExtra | ||
1145 | } | ||
1146 | newPath := make(cty.Path, len(path), newCap) | ||
1147 | copy(newPath, path) | ||
1148 | return newPath | ||
1149 | } | ||
1150 | |||
1151 | // ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil | ||
1152 | // or returns an empty value of a suitable type to serve as a placeholder for it. | ||
1153 | // | ||
1154 | // In particular, this function handles the special situation where a "list" is | ||
1155 | // actually represented as a tuple type where nested blocks contain | ||
1156 | // dynamically-typed values. | ||
1157 | func ctyNullBlockListAsEmpty(in cty.Value) cty.Value { | ||
1158 | if !in.IsNull() { | ||
1159 | return in | ||
1160 | } | ||
1161 | if ty := in.Type(); ty.IsListType() { | ||
1162 | return cty.ListValEmpty(ty.ElementType()) | ||
1163 | } | ||
1164 | return cty.EmptyTupleVal // must need a tuple, then | ||
1165 | } | ||
1166 | |||
1167 | // ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil | ||
1168 | // or returns an empty value of a suitable type to serve as a placeholder for it. | ||
1169 | // | ||
1170 | // In particular, this function handles the special situation where a "map" is | ||
1171 | // actually represented as an object type where nested blocks contain | ||
1172 | // dynamically-typed values. | ||
1173 | func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value { | ||
1174 | if !in.IsNull() { | ||
1175 | return in | ||
1176 | } | ||
1177 | if ty := in.Type(); ty.IsMapType() { | ||
1178 | return cty.MapValEmpty(ty.ElementType()) | ||
1179 | } | ||
1180 | return cty.EmptyObjectVal // must need an object, then | ||
1181 | } | ||
1182 | |||
1183 | // ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil | ||
1184 | // or returns an empty value of a suitable type to serve as a placeholder for it. | ||
1185 | func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value { | ||
1186 | if !in.IsNull() { | ||
1187 | return in | ||
1188 | } | ||
1189 | // Dynamically-typed attributes are not supported inside blocks backed by | ||
1190 | // sets, so our result here is always a set. | ||
1191 | return cty.SetValEmpty(in.Type().ElementType()) | ||
1192 | } | ||