10 "github.com/mitchellh/colorstring"
11 "github.com/zclconf/go-cty/cty"
12 ctyjson "github.com/zclconf/go-cty/cty/json"
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"
21 // ResourceChange returns a string representation of a change to a particular
22 // resource, for inclusion in user-facing plan output.
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
28 // If "color" is non-nil, it will be used to color the result. Otherwise,
29 // no color codes will be included.
31 change *plans.ResourceInstanceChangeSrc,
33 schema *configschema.Block,
34 color *colorstring.Colorize,
40 color = &colorstring.Colorize{
41 Colors: colorstring.DefaultColors,
47 dispAddr := addr.String()
48 if change.DeposedKey != states.NotDeposed {
49 dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey)
52 switch change.Action {
54 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr)))
56 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be read during apply\n # (config refers to values not yet known)", dispAddr)))
58 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr)))
59 case plans.CreateThenDelete, plans.DeleteThenCreate:
61 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced", dispAddr)))
63 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced", dispAddr)))
66 buf.WriteString(color.Color(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed", dispAddr)))
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))
71 buf.WriteString(color.Color("[reset]\n"))
73 switch change.Action {
75 buf.WriteString(color.Color("[green] +[reset] "))
77 buf.WriteString(color.Color("[cyan] <=[reset] "))
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] "))
85 buf.WriteString(color.Color("[red] -[reset] "))
87 buf.WriteString(color.Color("??? "))
90 switch addr.Resource.Resource.Mode {
91 case addrs.ManagedResourceMode:
92 buf.WriteString(fmt.Sprintf(
94 addr.Resource.Resource.Type,
95 addr.Resource.Resource.Name,
97 case addrs.DataResourceMode:
98 buf.WriteString(fmt.Sprintf(
100 addr.Resource.Resource.Type,
101 addr.Resource.Resource.Name,
104 // should never happen, since the above is exhaustive
105 buf.WriteString(addr.String())
108 buf.WriteString(" {")
110 p := blockBodyDiffPrinter{
113 action: change.Action,
114 requiredReplace: change.RequiredReplace,
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
121 path := make(cty.Path, 0, 3)
123 changeV, err := change.Decode(schema.ImpliedType())
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))
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
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)
140 bodyWritten := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path)
142 buf.WriteString("\n")
143 buf.WriteString(strings.Repeat(" ", 4))
145 buf.WriteString("}\n")
150 type blockBodyDiffPrinter struct {
152 color *colorstring.Colorize
154 requiredReplace cty.PathSet
157 const forcesNewResourceCaption = " [red]# forces replacement[reset]"
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)
165 blankBeforeBlocks := false
167 attrNames := make([]string, 0, len(schema.Attributes))
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.)
180 attrNames = append(attrNames, name)
181 if len(name) > attrNameLen {
182 attrNameLen = len(name)
185 sort.Strings(attrNames)
186 if len(attrNames) > 0 {
187 blankBeforeBlocks = true
190 for _, name := range attrNames {
191 attrS := schema.Attributes[name]
192 oldVal := ctyGetAttrMaybeNull(old, name)
193 newVal := ctyGetAttrMaybeNull(new, name)
196 p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path)
201 blockTypeNames := make([]string, 0, len(schema.BlockTypes))
202 for name := range schema.BlockTypes {
203 blockTypeNames = append(blockTypeNames, name)
205 sort.Strings(blockTypeNames)
207 for _, name := range blockTypeNames {
208 blockS := schema.BlockTypes[name]
209 oldVal := ctyGetAttrMaybeNull(old, name)
210 newVal := ctyGetAttrMaybeNull(new, name)
213 p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path)
215 // Always include a blank for any subsequent block types.
216 blankBeforeBlocks = true
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))
228 var action plans.Action
231 action = plans.Create
234 action = plans.Delete
235 case ctyEqualWithUnknown(old, new):
239 action = plans.Update
242 p.writeActionSymbol(action)
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(" = ")
251 p.buf.WriteString("(sensitive value)")
255 p.writeValue(new, action, indent+2)
256 if p.pathForcesNewResource(path) {
257 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
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)
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
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.
280 switch blockS.Nesting {
281 case configschema.NestingSingle, configschema.NestingGroup:
282 var action plans.Action
283 eqV := new.Equals(old)
286 action = plans.Create
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
299 p.buf.WriteRune('\n')
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)
309 oldItems := ctyCollectionValues(old)
310 newItems := ctyCollectionValues(new)
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.
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.
325 case len(oldItems) < len(newItems):
326 commonLen = len(oldItems)
328 commonLen = len(newItems)
331 if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) {
332 p.buf.WriteRune('\n')
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) {
343 p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path)
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)
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)
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)
364 oldItems := ctyCollectionValues(old)
365 newItems := ctyCollectionValues(new)
367 if (len(oldItems) + len(newItems)) == 0 {
368 // Nothing to do if both sets are empty
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)
378 p.buf.WriteRune('\n')
381 for it := all.ElementIterator(); it.Next(); {
382 _, val := it.Element()
383 var action plans.Action
384 var oldValue, newValue cty.Value
387 action = plans.Update
389 case !old.HasElement(val).True():
390 action = plans.Create
391 oldValue = cty.NullVal(val.Type())
393 case !new.HasElement(val).True():
394 action = plans.Delete
396 newValue = cty.NullVal(val.Type())
402 path := append(path, cty.IndexStep{Key: val})
403 p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path)
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)
413 oldItems := old.AsValueMap()
414 newItems := new.AsValueMap()
415 if (len(oldItems) + len(newItems)) == 0 {
416 // Nothing to do if both maps are empty
420 allKeys := make(map[string]bool)
421 for k := range oldItems {
424 for k := range newItems {
427 allKeysOrder := make([]string, 0, len(allKeys))
428 for k := range allKeys {
429 allKeysOrder = append(allKeysOrder, k)
431 sort.Strings(allKeysOrder)
434 p.buf.WriteRune('\n')
437 for _, k := range allKeysOrder {
438 var action plans.Action
439 oldValue := oldItems[k]
440 newValue := newItems[k]
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
454 path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
455 p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path)
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)
466 fmt.Fprintf(p.buf, "%s %q {", name, *label)
468 fmt.Fprintf(p.buf, "%s {", name)
471 if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
472 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
475 bodyWritten := p.writeBlockBodyDiff(blockS, old, new, indent+4, path)
477 p.buf.WriteString("\n")
478 p.buf.WriteString(strings.Repeat(" ", indent+2))
480 p.buf.WriteString("}")
483 func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
485 p.buf.WriteString("(known after apply)")
489 p.buf.WriteString(p.color.Color("[dark_gray]null[reset]"))
496 case ty.IsPrimitiveType():
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)
508 p.buf.WriteString("jsonencode(")
509 if jv.LengthInt() == 0 {
510 p.writeValue(jv, action, 0)
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))
519 break // don't *also* do the normal behavior below
523 fmt.Fprintf(p.buf, "%q", val.AsString())
526 p.buf.WriteString("true")
528 p.buf.WriteString("false")
531 bf := val.AsBigFloat()
532 p.buf.WriteString(bf.Text('f', -1))
534 // should never happen, since the above is exhaustive
535 fmt.Fprintf(p.buf, "%#v", val)
537 case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
538 p.buf.WriteString("[")
540 it := val.ElementIterator()
542 _, val := it.Element()
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(",")
551 if val.LengthInt() > 0 {
552 p.buf.WriteString("\n")
553 p.buf.WriteString(strings.Repeat(" ", indent))
555 p.buf.WriteString("]")
557 p.buf.WriteString("{")
560 for it := val.ElementIterator(); it.Next(); {
561 key, _ := it.Element()
562 if keyStr := key.AsString(); len(keyStr) > keyLen {
567 for it := val.ElementIterator(); it.Next(); {
568 key, val := it.Element()
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)
579 if val.LengthInt() > 0 {
580 p.buf.WriteString("\n")
581 p.buf.WriteString(strings.Repeat(" ", indent))
583 p.buf.WriteString("}")
584 case ty.IsObjectType():
585 p.buf.WriteString("{")
587 atys := ty.AttributeTypes()
588 attrNames := make([]string, 0, len(atys))
590 for attrName := range atys {
591 attrNames = append(attrNames, attrName)
592 if len(attrName) > nameLen {
593 nameLen = len(attrName)
596 sort.Strings(attrNames)
598 for _, attrName := range attrNames {
599 val := val.GetAttr(attrName)
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)
610 if len(attrNames) > 0 {
611 p.buf.WriteString("\n")
612 p.buf.WriteString(strings.Repeat(" ", indent))
614 p.buf.WriteString("}")
618 func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
620 typesEqual := ctyTypesEqual(ty, new.Type())
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 {
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()
640 // Special behavior for JSON strings containing object or
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))
660 // if they differ only in insigificant whitespace
661 // then we'll note that but still expand out the
663 if p.pathForcesNewResource(path) {
664 p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]"))
666 p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]"))
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))
680 if strings.Index(oldS, "\n") < 0 && strings.Index(newS, "\n") < 0 {
684 p.buf.WriteString("<<~EOT")
685 if p.pathForcesNewResource(path) {
686 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
688 p.buf.WriteString("\n")
690 var oldLines, newLines []cty.Value
692 r := strings.NewReader(oldS)
693 sc := bufio.NewScanner(r)
695 oldLines = append(oldLines, cty.StringVal(sc.Text()))
699 r := strings.NewReader(newS)
700 sc := bufio.NewScanner(r)
702 newLines = append(newLines, cty.StringVal(sc.Text()))
706 diffLines := ctySequenceDiff(oldLines, newLines)
707 for _, diffLine := range diffLines {
708 p.buf.WriteString(strings.Repeat(" ", indent+2))
709 p.writeActionSymbol(diffLine.Action)
711 switch diffLine.Action {
712 case plans.NoOp, plans.Delete:
713 p.buf.WriteString(diffLine.Before.AsString())
715 p.buf.WriteString(diffLine.After.AsString())
717 // Should never happen since the above covers all
718 // actions that ctySequenceDiff can return for strings
719 p.buf.WriteString(diffLine.After.AsString())
722 p.buf.WriteString("\n")
725 p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
726 p.buf.WriteString("EOT")
731 p.buf.WriteString("[")
732 if p.pathForcesNewResource(path) {
733 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
735 p.buf.WriteString("\n")
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)
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)
753 var all, added, removed cty.Value
754 if len(allVals) > 0 {
755 all = cty.SetVal(allVals)
757 all = cty.SetValEmpty(ty.ElementType())
759 if len(addedVals) > 0 {
760 added = cty.SetVal(addedVals)
762 added = cty.SetValEmpty(ty.ElementType())
764 if len(removedVals) > 0 {
765 removed = cty.SetVal(removedVals)
767 removed = cty.SetValEmpty(ty.ElementType())
770 for it := all.ElementIterator(); it.Next(); {
771 _, val := it.Element()
773 p.buf.WriteString(strings.Repeat(" ", indent+2))
775 var action plans.Action
778 action = plans.Update
779 case added.HasElement(val).True():
780 action = plans.Create
781 case removed.HasElement(val).True():
782 action = plans.Delete
787 p.writeActionSymbol(action)
788 p.writeValue(val, action, indent+4)
789 p.buf.WriteString(",\n")
792 p.buf.WriteString(strings.Repeat(" ", indent))
793 p.buf.WriteString("]")
795 case ty.IsListType() || ty.IsTupleType():
796 p.buf.WriteString("[")
797 if p.pathForcesNewResource(path) {
798 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
800 p.buf.WriteString("\n")
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)
810 p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path)
812 p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
814 // Should never happen since the above covers all
815 // actions that ctySequenceDiff can return.
816 p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
819 p.buf.WriteString(",\n")
822 p.buf.WriteString(strings.Repeat(" ", indent))
823 p.buf.WriteString("]")
827 p.buf.WriteString("{")
828 if p.pathForcesNewResource(path) {
829 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
831 p.buf.WriteString("\n")
835 for it := old.ElementIterator(); it.Next(); {
837 keyStr := k.AsString()
838 allKeys = append(allKeys, keyStr)
839 if len(keyStr) > keyLen {
843 for it := new.ElementIterator(); it.Next(); {
845 keyStr := k.AsString()
846 allKeys = append(allKeys, keyStr)
847 if len(keyStr) > keyLen {
852 sort.Strings(allKeys)
855 for i, k := range allKeys {
856 if i > 0 && lastK == k {
857 continue // skip duplicates (list is sorted)
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() {
871 action = plans.Update
874 path := append(path, cty.IndexStep{Key: kV})
876 p.writeActionSymbol(action)
877 p.writeValue(kV, action, indent+4)
878 p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
879 p.buf.WriteString(" = ")
881 case plans.Create, plans.NoOp:
883 p.writeValue(v, action, indent+4)
885 oldV := old.Index(kV)
886 newV := cty.NullVal(oldV.Type())
887 p.writeValueDiff(oldV, newV, indent+4, path)
889 oldV := old.Index(kV)
890 newV := new.Index(kV)
891 p.writeValueDiff(oldV, newV, indent+4, path)
894 p.buf.WriteByte('\n')
897 p.buf.WriteString(strings.Repeat(" ", indent))
898 p.buf.WriteString("}")
900 case ty.IsObjectType():
901 p.buf.WriteString("{")
902 p.buf.WriteString("\n")
904 forcesNewResource := p.pathForcesNewResource(path)
908 for it := old.ElementIterator(); it.Next(); {
910 keyStr := k.AsString()
911 allKeys = append(allKeys, keyStr)
912 if len(keyStr) > keyLen {
916 for it := new.ElementIterator(); it.Next(); {
918 keyStr := k.AsString()
919 allKeys = append(allKeys, keyStr)
920 if len(keyStr) > keyLen {
925 sort.Strings(allKeys)
928 for i, k := range allKeys {
929 if i > 0 && lastK == k {
930 continue // skip duplicates (list is sorted)
934 p.buf.WriteString(strings.Repeat(" ", indent+2))
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() {
944 action = plans.Update
947 path := append(path, cty.GetAttrStep{Name: kV})
949 p.writeActionSymbol(action)
951 p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
952 p.buf.WriteString(" = ")
955 case plans.Create, plans.NoOp:
957 p.writeValue(v, action, indent+4)
959 oldV := old.GetAttr(kV)
960 newV := cty.NullVal(oldV.Type())
961 p.writeValueDiff(oldV, newV, indent+4, path)
963 oldV := old.GetAttr(kV)
964 newV := new.GetAttr(kV)
965 p.writeValueDiff(oldV, newV, indent+4, path)
968 p.buf.WriteString("\n")
971 p.buf.WriteString(strings.Repeat(" ", indent))
972 p.buf.WriteString("}")
974 if forcesNewResource {
975 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
981 // In all other cases, we just show the new and old values as-is
982 p.writeValue(old, plans.Delete, indent)
984 p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] "))
986 p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
989 p.writeValue(new, plans.Create, indent)
990 if p.pathForcesNewResource(path) {
991 p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
995 // writeActionSymbol writes a symbol to represent the given action, followed
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) {
1003 p.buf.WriteString(p.color.Color("[green]+[reset] "))
1005 p.buf.WriteString(p.color.Color("[red]-[reset] "))
1007 p.buf.WriteString(p.color.Color("[yellow]~[reset] "))
1009 p.buf.WriteString(" ")
1011 // Should never happen
1012 p.buf.WriteString(p.color.Color("? "))
1016 func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool {
1017 if !p.action.IsReplace() {
1018 // "requiredReplace" only applies when the instance is being replaced
1021 return p.requiredReplace.Has(path)
1024 func ctyEmptyString(value cty.Value) bool {
1025 if !value.IsNull() && value.IsKnown() {
1026 valueType := value.Type()
1027 if valueType == cty.String && value.AsString() == "" {
1034 func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
1035 attrType := val.Type().AttributeType(name)
1038 return cty.NullVal(attrType)
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)
1053 func ctyCollectionValues(val cty.Value) []cty.Value {
1054 if !val.IsKnown() || val.IsNull() {
1058 ret := make([]cty.Value, 0, val.LengthInt())
1059 for it := val.ElementIterator(); it.Next(); {
1060 _, value := it.Element()
1061 ret = append(ret, value)
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,
1082 newI++ // we also consume the next "new" in this case
1086 ret = append(ret, &plans.Change{
1087 Action: plans.Delete,
1089 After: cty.NullVal(old[oldI].Type()),
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()),
1101 if lcsI < len(lcs) {
1102 ret = append(ret, &plans.Change{
1108 // All of our indexes advance together now, since the line
1109 // is common to all three sequences.
1118 func ctyEqualWithUnknown(old, new cty.Value) bool {
1119 if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
1122 return old.Equals(new).True()
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() {
1132 if oldT.IsTupleType() && newT.IsTupleType() {
1135 return oldT.Equals(newT)
1138 func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
1139 if cap(path)-len(path) >= minExtra {
1142 newCap := cap(path) * 2
1143 if newCap < (len(path) + minExtra) {
1144 newCap = len(path) + minExtra
1146 newPath := make(cty.Path, len(path), newCap)
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.
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 {
1161 if ty := in.Type(); ty.IsListType() {
1162 return cty.ListValEmpty(ty.ElementType())
1164 return cty.EmptyTupleVal // must need a tuple, then
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.
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 {
1177 if ty := in.Type(); ty.IsMapType() {
1178 return cty.MapValEmpty(ty.ElementType())
1180 return cty.EmptyObjectVal // must need an object, then
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 {
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())