github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/command/format/diff.go (about)

     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-plugin-sdk/internal/addrs"
    15  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
    16  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans"
    17  	"github.com/hashicorp/terraform-plugin-sdk/internal/plans/objchange"
    18  	"github.com/hashicorp/terraform-plugin-sdk/internal/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  }