kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/format/diff.go (about)

     1  package format
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"log"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/mitchellh/colorstring"
    12  	"github.com/zclconf/go-cty/cty"
    13  	ctyjson "github.com/zclconf/go-cty/cty/json"
    14  
    15  	"kubeform.dev/terraform-backend-sdk/addrs"
    16  	"kubeform.dev/terraform-backend-sdk/configs/configschema"
    17  	"kubeform.dev/terraform-backend-sdk/lang/marks"
    18  	"kubeform.dev/terraform-backend-sdk/plans"
    19  	"kubeform.dev/terraform-backend-sdk/plans/objchange"
    20  	"kubeform.dev/terraform-backend-sdk/states"
    21  )
    22  
    23  // DiffLanguage controls the description of the resource change reasons.
    24  type DiffLanguage rune
    25  
    26  //go:generate go run golang.org/x/tools/cmd/stringer -type=DiffLanguage diff.go
    27  
    28  const (
    29  	// DiffLanguageProposedChange indicates that the change is one which is
    30  	// planned to be applied.
    31  	DiffLanguageProposedChange DiffLanguage = 'P'
    32  
    33  	// DiffLanguageDetectedDrift indicates that the change is detected drift
    34  	// from the configuration.
    35  	DiffLanguageDetectedDrift DiffLanguage = 'D'
    36  )
    37  
    38  // ResourceChange returns a string representation of a change to a particular
    39  // resource, for inclusion in user-facing plan output.
    40  //
    41  // The resource schema must be provided along with the change so that the
    42  // formatted change can reflect the configuration structure for the associated
    43  // resource.
    44  //
    45  // If "color" is non-nil, it will be used to color the result. Otherwise,
    46  // no color codes will be included.
    47  func ResourceChange(
    48  	change *plans.ResourceInstanceChangeSrc,
    49  	schema *configschema.Block,
    50  	color *colorstring.Colorize,
    51  	language DiffLanguage,
    52  ) string {
    53  	addr := change.Addr
    54  	var buf bytes.Buffer
    55  
    56  	if color == nil {
    57  		color = &colorstring.Colorize{
    58  			Colors:  colorstring.DefaultColors,
    59  			Disable: true,
    60  			Reset:   false,
    61  		}
    62  	}
    63  
    64  	dispAddr := addr.String()
    65  	if change.DeposedKey != states.NotDeposed {
    66  		dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey)
    67  	}
    68  
    69  	switch change.Action {
    70  	case plans.Create:
    71  		buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be created"), dispAddr))
    72  	case plans.Read:
    73  		buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be read during apply\n  # (config refers to values not yet known)"), dispAddr))
    74  	case plans.Update:
    75  		switch language {
    76  		case DiffLanguageProposedChange:
    77  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be updated in-place"), dispAddr))
    78  		case DiffLanguageDetectedDrift:
    79  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] has changed"), dispAddr))
    80  		default:
    81  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] update (unknown reason %s)"), dispAddr, language))
    82  		}
    83  	case plans.CreateThenDelete, plans.DeleteThenCreate:
    84  		switch change.ActionReason {
    85  		case plans.ResourceInstanceReplaceBecauseTainted:
    86  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] is tainted, so must be [bold][red]replaced"), dispAddr))
    87  		case plans.ResourceInstanceReplaceByRequest:
    88  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be [bold][red]replaced[reset], as requested"), dispAddr))
    89  		default:
    90  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] must be [bold][red]replaced"), dispAddr))
    91  		}
    92  	case plans.Delete:
    93  		switch language {
    94  		case DiffLanguageProposedChange:
    95  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] will be [bold][red]destroyed"), dispAddr))
    96  		case DiffLanguageDetectedDrift:
    97  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] has been deleted"), dispAddr))
    98  		default:
    99  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] delete (unknown reason %s)"), dispAddr, language))
   100  		}
   101  		if change.DeposedKey != states.NotDeposed {
   102  			// Some extra context about this unusual situation.
   103  			buf.WriteString(color.Color("\n  # (left over from a partially-failed replacement of this instance)"))
   104  		}
   105  	case plans.NoOp:
   106  		if change.Moved() {
   107  			buf.WriteString(fmt.Sprintf(color.Color("[bold]  # %s[reset] has moved to [bold]%s[reset]"), change.PrevRunAddr.String(), dispAddr))
   108  			break
   109  		}
   110  		fallthrough
   111  	default:
   112  		// should never happen, since the above is exhaustive
   113  		buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr))
   114  	}
   115  	buf.WriteString(color.Color("[reset]\n"))
   116  
   117  	if change.Moved() && change.Action != plans.NoOp {
   118  		buf.WriteString(fmt.Sprintf(color.Color("[bold]  # [reset]([bold]%s[reset] has moved to [bold]%s[reset])\n"), change.PrevRunAddr.String(), dispAddr))
   119  	}
   120  
   121  	if change.Moved() && change.Action == plans.NoOp {
   122  		buf.WriteString("    ")
   123  	} else {
   124  		buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ")
   125  	}
   126  
   127  	switch addr.Resource.Resource.Mode {
   128  	case addrs.ManagedResourceMode:
   129  		buf.WriteString(fmt.Sprintf(
   130  			"resource %q %q",
   131  			addr.Resource.Resource.Type,
   132  			addr.Resource.Resource.Name,
   133  		))
   134  	case addrs.DataResourceMode:
   135  		buf.WriteString(fmt.Sprintf(
   136  			"data %q %q ",
   137  			addr.Resource.Resource.Type,
   138  			addr.Resource.Resource.Name,
   139  		))
   140  	default:
   141  		// should never happen, since the above is exhaustive
   142  		buf.WriteString(addr.String())
   143  	}
   144  
   145  	buf.WriteString(" {")
   146  
   147  	p := blockBodyDiffPrinter{
   148  		buf:             &buf,
   149  		color:           color,
   150  		action:          change.Action,
   151  		requiredReplace: change.RequiredReplace,
   152  	}
   153  
   154  	// Most commonly-used resources have nested blocks that result in us
   155  	// going at least three traversals deep while we recurse here, so we'll
   156  	// start with that much capacity and then grow as needed for deeper
   157  	// structures.
   158  	path := make(cty.Path, 0, 3)
   159  
   160  	changeV, err := change.Decode(schema.ImpliedType())
   161  	if err != nil {
   162  		// Should never happen in here, since we've already been through
   163  		// loads of layers of encode/decode of the planned changes before now.
   164  		panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err))
   165  	}
   166  
   167  	// We currently have an opt-out that permits the legacy SDK to return values
   168  	// that defy our usual conventions around handling of nesting blocks. To
   169  	// avoid the rendering code from needing to handle all of these, we'll
   170  	// normalize first.
   171  	// (Ideally we'd do this as part of the SDK opt-out implementation in core,
   172  	// but we've added it here for now to reduce risk of unexpected impacts
   173  	// on other code in core.)
   174  	changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema)
   175  	changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema)
   176  
   177  	result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path)
   178  	if result.bodyWritten {
   179  		buf.WriteString("\n")
   180  		buf.WriteString(strings.Repeat(" ", 4))
   181  	}
   182  	buf.WriteString("}\n")
   183  
   184  	return buf.String()
   185  }
   186  
   187  // OutputChanges returns a string representation of a set of changes to output
   188  // values for inclusion in user-facing plan output.
   189  //
   190  // If "color" is non-nil, it will be used to color the result. Otherwise,
   191  // no color codes will be included.
   192  func OutputChanges(
   193  	changes []*plans.OutputChangeSrc,
   194  	color *colorstring.Colorize,
   195  ) string {
   196  	var buf bytes.Buffer
   197  	p := blockBodyDiffPrinter{
   198  		buf:    &buf,
   199  		color:  color,
   200  		action: plans.Update, // not actually used in this case, because we're not printing a containing block
   201  	}
   202  
   203  	// We're going to reuse the codepath we used for printing resource block
   204  	// diffs, by pretending that the set of defined outputs are the attributes
   205  	// of some resource. It's a little forced to do this, but it gives us all
   206  	// the same formatting heuristics as we normally use for resource
   207  	// attributes.
   208  	oldVals := make(map[string]cty.Value, len(changes))
   209  	newVals := make(map[string]cty.Value, len(changes))
   210  	synthSchema := &configschema.Block{
   211  		Attributes: make(map[string]*configschema.Attribute, len(changes)),
   212  	}
   213  	for _, changeSrc := range changes {
   214  		name := changeSrc.Addr.OutputValue.Name
   215  		change, err := changeSrc.Decode()
   216  		if err != nil {
   217  			// It'd be weird to get a decoding error here because that would
   218  			// suggest that Terraform itself just produced an invalid plan, and
   219  			// we don't have any good way to ignore it in this codepath, so
   220  			// we'll just log it and ignore it.
   221  			log.Printf("[ERROR] format.OutputChanges: Failed to decode planned change for output %q: %s", name, err)
   222  			continue
   223  		}
   224  		synthSchema.Attributes[name] = &configschema.Attribute{
   225  			Type:      cty.DynamicPseudoType, // output types are decided dynamically based on the given value
   226  			Optional:  true,
   227  			Sensitive: change.Sensitive,
   228  		}
   229  		oldVals[name] = change.Before
   230  		newVals[name] = change.After
   231  	}
   232  
   233  	p.writeBlockBodyDiff(synthSchema, cty.ObjectVal(oldVals), cty.ObjectVal(newVals), 2, nil)
   234  
   235  	return buf.String()
   236  }
   237  
   238  type blockBodyDiffPrinter struct {
   239  	buf             *bytes.Buffer
   240  	color           *colorstring.Colorize
   241  	action          plans.Action
   242  	requiredReplace cty.PathSet
   243  	// verbose is set to true when using the "diff" printer to format state
   244  	verbose bool
   245  }
   246  
   247  type blockBodyDiffResult struct {
   248  	bodyWritten       bool
   249  	skippedAttributes int
   250  	skippedBlocks     int
   251  }
   252  
   253  const forcesNewResourceCaption = " [red]# forces replacement[reset]"
   254  
   255  // writeBlockBodyDiff writes attribute or block differences
   256  // and returns true if any differences were found and written
   257  func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult {
   258  	path = ctyEnsurePathCapacity(path, 1)
   259  	result := blockBodyDiffResult{}
   260  
   261  	// write the attributes diff
   262  	blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result)
   263  	p.writeSkippedAttr(result.skippedAttributes, indent+2)
   264  
   265  	{
   266  		blockTypeNames := make([]string, 0, len(schema.BlockTypes))
   267  		for name := range schema.BlockTypes {
   268  			blockTypeNames = append(blockTypeNames, name)
   269  		}
   270  		sort.Strings(blockTypeNames)
   271  
   272  		for _, name := range blockTypeNames {
   273  			blockS := schema.BlockTypes[name]
   274  			oldVal := ctyGetAttrMaybeNull(old, name)
   275  			newVal := ctyGetAttrMaybeNull(new, name)
   276  
   277  			result.bodyWritten = true
   278  			skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path)
   279  			if skippedBlocks > 0 {
   280  				result.skippedBlocks += skippedBlocks
   281  			}
   282  
   283  			// Always include a blank for any subsequent block types.
   284  			blankBeforeBlocks = true
   285  		}
   286  		if result.skippedBlocks > 0 {
   287  			noun := "blocks"
   288  			if result.skippedBlocks == 1 {
   289  				noun = "block"
   290  			}
   291  			p.buf.WriteString("\n")
   292  			p.buf.WriteString(strings.Repeat(" ", indent+2))
   293  			p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), result.skippedBlocks, noun))
   294  		}
   295  	}
   296  
   297  	return result
   298  }
   299  
   300  func (p *blockBodyDiffPrinter) writeAttrsDiff(
   301  	attrsS map[string]*configschema.Attribute,
   302  	old, new cty.Value,
   303  	indent int,
   304  	path cty.Path,
   305  	result *blockBodyDiffResult) bool {
   306  
   307  	blankBeforeBlocks := false
   308  
   309  	attrNames := make([]string, 0, len(attrsS))
   310  	attrNameLen := 0
   311  	for name := range attrsS {
   312  		oldVal := ctyGetAttrMaybeNull(old, name)
   313  		newVal := ctyGetAttrMaybeNull(new, name)
   314  		if oldVal.IsNull() && newVal.IsNull() {
   315  			// Skip attributes where both old and new values are null
   316  			// (we do this early here so that we'll do our value alignment
   317  			// based on the longest attribute name that has a change, rather
   318  			// than the longest attribute name in the full set.)
   319  			continue
   320  		}
   321  
   322  		attrNames = append(attrNames, name)
   323  		if len(name) > attrNameLen {
   324  			attrNameLen = len(name)
   325  		}
   326  	}
   327  	sort.Strings(attrNames)
   328  	if len(attrNames) > 0 {
   329  		blankBeforeBlocks = true
   330  	}
   331  
   332  	for _, name := range attrNames {
   333  		attrS := attrsS[name]
   334  		oldVal := ctyGetAttrMaybeNull(old, name)
   335  		newVal := ctyGetAttrMaybeNull(new, name)
   336  
   337  		result.bodyWritten = true
   338  		skipped := p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path)
   339  		if skipped {
   340  			result.skippedAttributes++
   341  		}
   342  	}
   343  
   344  	return blankBeforeBlocks
   345  }
   346  
   347  // getPlanActionAndShow returns the action value
   348  // and a boolean for showJustNew. In this function we
   349  // modify the old and new values to remove any possible marks
   350  func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) {
   351  	var action plans.Action
   352  	showJustNew := false
   353  	switch {
   354  	case old.IsNull():
   355  		action = plans.Create
   356  		showJustNew = true
   357  	case new.IsNull():
   358  		action = plans.Delete
   359  	case ctyEqualWithUnknown(old, new):
   360  		action = plans.NoOp
   361  		showJustNew = true
   362  	default:
   363  		action = plans.Update
   364  	}
   365  	return action, showJustNew
   366  }
   367  
   368  func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool {
   369  	path = append(path, cty.GetAttrStep{Name: name})
   370  	action, showJustNew := getPlanActionAndShow(old, new)
   371  
   372  	if action == plans.NoOp && !p.verbose && !identifyingAttribute(name, attrS) {
   373  		return true
   374  	}
   375  
   376  	if attrS.NestedType != nil {
   377  		p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew)
   378  		return false
   379  	}
   380  
   381  	p.buf.WriteString("\n")
   382  
   383  	p.writeSensitivityWarning(old, new, indent, action, false)
   384  
   385  	p.buf.WriteString(strings.Repeat(" ", indent))
   386  	p.writeActionSymbol(action)
   387  
   388  	p.buf.WriteString(p.color.Color("[bold]"))
   389  	p.buf.WriteString(name)
   390  	p.buf.WriteString(p.color.Color("[reset]"))
   391  	p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
   392  	p.buf.WriteString(" = ")
   393  
   394  	if attrS.Sensitive {
   395  		p.buf.WriteString("(sensitive value)")
   396  		if p.pathForcesNewResource(path) {
   397  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   398  		}
   399  	} else {
   400  		switch {
   401  		case showJustNew:
   402  			p.writeValue(new, action, indent+2)
   403  			if p.pathForcesNewResource(path) {
   404  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   405  			}
   406  		default:
   407  			// We show new even if it is null to emphasize the fact
   408  			// that it is being unset, since otherwise it is easy to
   409  			// misunderstand that the value is still set to the old value.
   410  			p.writeValueDiff(old, new, indent+2, path)
   411  		}
   412  	}
   413  
   414  	return false
   415  }
   416  
   417  // writeNestedAttrDiff is responsible for formatting Attributes with NestedTypes
   418  // in the diff.
   419  func (p *blockBodyDiffPrinter) writeNestedAttrDiff(
   420  	name string, objS *configschema.Object, old, new cty.Value,
   421  	nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) {
   422  
   423  	p.buf.WriteString("\n")
   424  	p.buf.WriteString(strings.Repeat(" ", indent))
   425  	p.writeActionSymbol(action)
   426  
   427  	p.buf.WriteString(p.color.Color("[bold]"))
   428  	p.buf.WriteString(name)
   429  	p.buf.WriteString(p.color.Color("[reset]"))
   430  	p.buf.WriteString(strings.Repeat(" ", nameLen-len(name)))
   431  
   432  	result := &blockBodyDiffResult{}
   433  	switch objS.Nesting {
   434  	case configschema.NestingSingle:
   435  		p.buf.WriteString(" = {")
   436  		if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   437  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   438  		}
   439  		p.writeAttrsDiff(objS.Attributes, old, new, indent+2, path, result)
   440  		p.writeSkippedAttr(result.skippedAttributes, indent+4)
   441  		p.buf.WriteString("\n")
   442  		p.buf.WriteString(strings.Repeat(" ", indent))
   443  		p.buf.WriteString("}")
   444  
   445  	case configschema.NestingList:
   446  		p.buf.WriteString(" = [")
   447  		if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   448  			p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   449  		}
   450  		p.buf.WriteString("\n")
   451  		p.buf.WriteString(strings.Repeat(" ", indent+4))
   452  		p.writeActionSymbol(action)
   453  		p.buf.WriteString("{")
   454  
   455  		oldItems := ctyCollectionValues(old)
   456  		newItems := ctyCollectionValues(new)
   457  		// Here we intentionally preserve the index-based correspondance
   458  		// between old and new, rather than trying to detect insertions
   459  		// and removals in the list, because this more accurately reflects
   460  		// how Terraform Core and providers will understand the change,
   461  		// particularly when the nested block contains computed attributes
   462  		// that will themselves maintain correspondance by index.
   463  
   464  		// commonLen is number of elements that exist in both lists, which
   465  		// will be presented as updates (~). Any additional items in one
   466  		// of the lists will be presented as either creates (+) or deletes (-)
   467  		// depending on which list they belong to.
   468  		var commonLen int
   469  		// unchanged is the number of unchanged elements
   470  		var unchanged int
   471  
   472  		switch {
   473  		case len(oldItems) < len(newItems):
   474  			commonLen = len(oldItems)
   475  		default:
   476  			commonLen = len(newItems)
   477  		}
   478  		for i := 0; i < commonLen; i++ {
   479  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   480  			oldItem := oldItems[i]
   481  			newItem := newItems[i]
   482  			if oldItem.RawEquals(newItem) {
   483  				action = plans.NoOp
   484  				unchanged++
   485  			}
   486  			if action != plans.NoOp {
   487  				p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
   488  				p.writeSkippedAttr(result.skippedAttributes, indent+8)
   489  				p.buf.WriteString("\n")
   490  			}
   491  		}
   492  		for i := commonLen; i < len(oldItems); i++ {
   493  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   494  			oldItem := oldItems[i]
   495  			newItem := cty.NullVal(oldItem.Type())
   496  			p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
   497  			p.buf.WriteString("\n")
   498  		}
   499  		for i := commonLen; i < len(newItems); i++ {
   500  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   501  			newItem := newItems[i]
   502  			oldItem := cty.NullVal(newItem.Type())
   503  			p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+6, path, result)
   504  			p.buf.WriteString("\n")
   505  		}
   506  		p.buf.WriteString(strings.Repeat(" ", indent+4))
   507  		p.buf.WriteString("},\n")
   508  		p.writeSkippedElems(unchanged, indent+4)
   509  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   510  		p.buf.WriteString("]")
   511  
   512  		if !new.IsKnown() {
   513  			p.buf.WriteString(" -> (known after apply)")
   514  		}
   515  
   516  	case configschema.NestingSet:
   517  		oldItems := ctyCollectionValues(old)
   518  		newItems := ctyCollectionValues(new)
   519  
   520  		var all cty.Value
   521  		if len(oldItems)+len(newItems) > 0 {
   522  			allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
   523  			allItems = append(allItems, oldItems...)
   524  			allItems = append(allItems, newItems...)
   525  
   526  			all = cty.SetVal(allItems)
   527  		} else {
   528  			all = cty.SetValEmpty(old.Type().ElementType())
   529  		}
   530  
   531  		p.buf.WriteString(" = [")
   532  
   533  		for it := all.ElementIterator(); it.Next(); {
   534  			_, val := it.Element()
   535  			var action plans.Action
   536  			var oldValue, newValue cty.Value
   537  			switch {
   538  			case !val.IsKnown():
   539  				action = plans.Update
   540  				newValue = val
   541  			case !new.IsKnown():
   542  				action = plans.Delete
   543  				// the value must have come from the old set
   544  				oldValue = val
   545  				// Mark the new val as null, but the entire set will be
   546  				// displayed as "(unknown after apply)"
   547  				newValue = cty.NullVal(val.Type())
   548  			case old.IsNull() || !old.HasElement(val).True():
   549  				action = plans.Create
   550  				oldValue = cty.NullVal(val.Type())
   551  				newValue = val
   552  			case new.IsNull() || !new.HasElement(val).True():
   553  				action = plans.Delete
   554  				oldValue = val
   555  				newValue = cty.NullVal(val.Type())
   556  			default:
   557  				action = plans.NoOp
   558  				oldValue = val
   559  				newValue = val
   560  			}
   561  
   562  			p.buf.WriteString("\n")
   563  			p.buf.WriteString(strings.Repeat(" ", indent+4))
   564  			p.writeActionSymbol(action)
   565  			p.buf.WriteString("{")
   566  
   567  			if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   568  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   569  			}
   570  
   571  			path := append(path, cty.IndexStep{Key: val})
   572  			p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result)
   573  
   574  			p.buf.WriteString("\n")
   575  			p.buf.WriteString(strings.Repeat(" ", indent+4))
   576  			p.buf.WriteString("},")
   577  		}
   578  		p.buf.WriteString("\n")
   579  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   580  		p.buf.WriteString("]")
   581  
   582  		if !new.IsKnown() {
   583  			p.buf.WriteString(" -> (known after apply)")
   584  		}
   585  
   586  	case configschema.NestingMap:
   587  		// For the sake of handling nested blocks, we'll treat a null map
   588  		// the same as an empty map since the config language doesn't
   589  		// distinguish these anyway.
   590  		old = ctyNullBlockMapAsEmpty(old)
   591  		new = ctyNullBlockMapAsEmpty(new)
   592  
   593  		oldItems := old.AsValueMap()
   594  
   595  		newItems := map[string]cty.Value{}
   596  
   597  		if new.IsKnown() {
   598  			newItems = new.AsValueMap()
   599  		}
   600  
   601  		allKeys := make(map[string]bool)
   602  		for k := range oldItems {
   603  			allKeys[k] = true
   604  		}
   605  		for k := range newItems {
   606  			allKeys[k] = true
   607  		}
   608  		allKeysOrder := make([]string, 0, len(allKeys))
   609  		for k := range allKeys {
   610  			allKeysOrder = append(allKeysOrder, k)
   611  		}
   612  		sort.Strings(allKeysOrder)
   613  
   614  		p.buf.WriteString(" = {\n")
   615  
   616  		// unchanged tracks the number of unchanged elements
   617  		unchanged := 0
   618  		for _, k := range allKeysOrder {
   619  			var action plans.Action
   620  			oldValue := oldItems[k]
   621  
   622  			newValue := newItems[k]
   623  			switch {
   624  			case oldValue == cty.NilVal:
   625  				oldValue = cty.NullVal(newValue.Type())
   626  				action = plans.Create
   627  			case newValue == cty.NilVal:
   628  				newValue = cty.NullVal(oldValue.Type())
   629  				action = plans.Delete
   630  			case !newValue.RawEquals(oldValue):
   631  				action = plans.Update
   632  			default:
   633  				action = plans.NoOp
   634  				unchanged++
   635  			}
   636  
   637  			if action != plans.NoOp {
   638  				p.buf.WriteString(strings.Repeat(" ", indent+4))
   639  				p.writeActionSymbol(action)
   640  				fmt.Fprintf(p.buf, "%q = {", k)
   641  				if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) {
   642  					p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   643  				}
   644  
   645  				path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
   646  				p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+6, path, result)
   647  				p.writeSkippedAttr(result.skippedAttributes, indent+8)
   648  				p.buf.WriteString("\n")
   649  				p.buf.WriteString(strings.Repeat(" ", indent+4))
   650  				p.buf.WriteString("},\n")
   651  			}
   652  		}
   653  
   654  		p.writeSkippedElems(unchanged, indent+4)
   655  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   656  		p.buf.WriteString("}")
   657  		if !new.IsKnown() {
   658  			p.buf.WriteString(" -> (known after apply)")
   659  		}
   660  	}
   661  }
   662  
   663  func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int {
   664  	skippedBlocks := 0
   665  	path = append(path, cty.GetAttrStep{Name: name})
   666  	if old.IsNull() && new.IsNull() {
   667  		// Nothing to do if both old and new is null
   668  		return skippedBlocks
   669  	}
   670  
   671  	// If either the old or the new value is marked,
   672  	// Display a special diff because it is irrelevant
   673  	// to list all obfuscated attributes as (sensitive)
   674  	if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
   675  		p.writeSensitiveNestedBlockDiff(name, old, new, indent, blankBefore, path)
   676  		return 0
   677  	}
   678  
   679  	// Where old/new are collections representing a nesting mode other than
   680  	// NestingSingle, we assume the collection value can never be unknown
   681  	// since we always produce the container for the nested objects, even if
   682  	// the objects within are computed.
   683  
   684  	switch blockS.Nesting {
   685  	case configschema.NestingSingle, configschema.NestingGroup:
   686  		var action plans.Action
   687  		eqV := new.Equals(old)
   688  		switch {
   689  		case old.IsNull():
   690  			action = plans.Create
   691  		case new.IsNull():
   692  			action = plans.Delete
   693  		case !new.IsWhollyKnown() || !old.IsWhollyKnown():
   694  			// "old" should actually always be known due to our contract
   695  			// that old values must never be unknown, but we'll allow it
   696  			// anyway to be robust.
   697  			action = plans.Update
   698  		case !eqV.IsKnown() || !eqV.True():
   699  			action = plans.Update
   700  		}
   701  
   702  		if blankBefore {
   703  			p.buf.WriteRune('\n')
   704  		}
   705  		skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path)
   706  		if skipped {
   707  			return 1
   708  		}
   709  	case configschema.NestingList:
   710  		// For the sake of handling nested blocks, we'll treat a null list
   711  		// the same as an empty list since the config language doesn't
   712  		// distinguish these anyway.
   713  		old = ctyNullBlockListAsEmpty(old)
   714  		new = ctyNullBlockListAsEmpty(new)
   715  
   716  		oldItems := ctyCollectionValues(old)
   717  		newItems := ctyCollectionValues(new)
   718  
   719  		// Here we intentionally preserve the index-based correspondance
   720  		// between old and new, rather than trying to detect insertions
   721  		// and removals in the list, because this more accurately reflects
   722  		// how Terraform Core and providers will understand the change,
   723  		// particularly when the nested block contains computed attributes
   724  		// that will themselves maintain correspondance by index.
   725  
   726  		// commonLen is number of elements that exist in both lists, which
   727  		// will be presented as updates (~). Any additional items in one
   728  		// of the lists will be presented as either creates (+) or deletes (-)
   729  		// depending on which list they belong to.
   730  		var commonLen int
   731  		switch {
   732  		case len(oldItems) < len(newItems):
   733  			commonLen = len(oldItems)
   734  		default:
   735  			commonLen = len(newItems)
   736  		}
   737  
   738  		if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) {
   739  			p.buf.WriteRune('\n')
   740  		}
   741  
   742  		for i := 0; i < commonLen; i++ {
   743  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   744  			oldItem := oldItems[i]
   745  			newItem := newItems[i]
   746  			action := plans.Update
   747  			if oldItem.RawEquals(newItem) {
   748  				action = plans.NoOp
   749  			}
   750  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path)
   751  			if skipped {
   752  				skippedBlocks++
   753  			}
   754  		}
   755  		for i := commonLen; i < len(oldItems); i++ {
   756  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   757  			oldItem := oldItems[i]
   758  			newItem := cty.NullVal(oldItem.Type())
   759  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path)
   760  			if skipped {
   761  				skippedBlocks++
   762  			}
   763  		}
   764  		for i := commonLen; i < len(newItems); i++ {
   765  			path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))})
   766  			newItem := newItems[i]
   767  			oldItem := cty.NullVal(newItem.Type())
   768  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path)
   769  			if skipped {
   770  				skippedBlocks++
   771  			}
   772  		}
   773  	case configschema.NestingSet:
   774  		// For the sake of handling nested blocks, we'll treat a null set
   775  		// the same as an empty set since the config language doesn't
   776  		// distinguish these anyway.
   777  		old = ctyNullBlockSetAsEmpty(old)
   778  		new = ctyNullBlockSetAsEmpty(new)
   779  
   780  		oldItems := ctyCollectionValues(old)
   781  		newItems := ctyCollectionValues(new)
   782  
   783  		if (len(oldItems) + len(newItems)) == 0 {
   784  			// Nothing to do if both sets are empty
   785  			return 0
   786  		}
   787  
   788  		allItems := make([]cty.Value, 0, len(oldItems)+len(newItems))
   789  		allItems = append(allItems, oldItems...)
   790  		allItems = append(allItems, newItems...)
   791  		all := cty.SetVal(allItems)
   792  
   793  		if blankBefore {
   794  			p.buf.WriteRune('\n')
   795  		}
   796  
   797  		for it := all.ElementIterator(); it.Next(); {
   798  			_, val := it.Element()
   799  			var action plans.Action
   800  			var oldValue, newValue cty.Value
   801  			switch {
   802  			case !val.IsKnown():
   803  				action = plans.Update
   804  				newValue = val
   805  			case !old.HasElement(val).True():
   806  				action = plans.Create
   807  				oldValue = cty.NullVal(val.Type())
   808  				newValue = val
   809  			case !new.HasElement(val).True():
   810  				action = plans.Delete
   811  				oldValue = val
   812  				newValue = cty.NullVal(val.Type())
   813  			default:
   814  				action = plans.NoOp
   815  				oldValue = val
   816  				newValue = val
   817  			}
   818  			path := append(path, cty.IndexStep{Key: val})
   819  			skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path)
   820  			if skipped {
   821  				skippedBlocks++
   822  			}
   823  		}
   824  
   825  	case configschema.NestingMap:
   826  		// For the sake of handling nested blocks, we'll treat a null map
   827  		// the same as an empty map since the config language doesn't
   828  		// distinguish these anyway.
   829  		old = ctyNullBlockMapAsEmpty(old)
   830  		new = ctyNullBlockMapAsEmpty(new)
   831  
   832  		oldItems := old.AsValueMap()
   833  		newItems := new.AsValueMap()
   834  		if (len(oldItems) + len(newItems)) == 0 {
   835  			// Nothing to do if both maps are empty
   836  			return 0
   837  		}
   838  
   839  		allKeys := make(map[string]bool)
   840  		for k := range oldItems {
   841  			allKeys[k] = true
   842  		}
   843  		for k := range newItems {
   844  			allKeys[k] = true
   845  		}
   846  		allKeysOrder := make([]string, 0, len(allKeys))
   847  		for k := range allKeys {
   848  			allKeysOrder = append(allKeysOrder, k)
   849  		}
   850  		sort.Strings(allKeysOrder)
   851  
   852  		if blankBefore {
   853  			p.buf.WriteRune('\n')
   854  		}
   855  
   856  		for _, k := range allKeysOrder {
   857  			var action plans.Action
   858  			oldValue := oldItems[k]
   859  			newValue := newItems[k]
   860  			switch {
   861  			case oldValue == cty.NilVal:
   862  				oldValue = cty.NullVal(newValue.Type())
   863  				action = plans.Create
   864  			case newValue == cty.NilVal:
   865  				newValue = cty.NullVal(oldValue.Type())
   866  				action = plans.Delete
   867  			case !newValue.RawEquals(oldValue):
   868  				action = plans.Update
   869  			default:
   870  				action = plans.NoOp
   871  			}
   872  
   873  			path := append(path, cty.IndexStep{Key: cty.StringVal(k)})
   874  			skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path)
   875  			if skipped {
   876  				skippedBlocks++
   877  			}
   878  		}
   879  	}
   880  	return skippedBlocks
   881  }
   882  
   883  func (p *blockBodyDiffPrinter) writeSensitiveNestedBlockDiff(name string, old, new cty.Value, indent int, blankBefore bool, path cty.Path) {
   884  	var action plans.Action
   885  	switch {
   886  	case old.IsNull():
   887  		action = plans.Create
   888  	case new.IsNull():
   889  		action = plans.Delete
   890  	case !new.IsWhollyKnown() || !old.IsWhollyKnown():
   891  		// "old" should actually always be known due to our contract
   892  		// that old values must never be unknown, but we'll allow it
   893  		// anyway to be robust.
   894  		action = plans.Update
   895  	case !ctyEqualValueAndMarks(old, new):
   896  		action = plans.Update
   897  	}
   898  
   899  	if blankBefore {
   900  		p.buf.WriteRune('\n')
   901  	}
   902  
   903  	// New line before warning printing
   904  	p.buf.WriteRune('\n')
   905  	p.writeSensitivityWarning(old, new, indent, action, true)
   906  	p.buf.WriteString(strings.Repeat(" ", indent))
   907  	p.writeActionSymbol(action)
   908  	fmt.Fprintf(p.buf, "%s {", name)
   909  	if action != plans.NoOp && p.pathForcesNewResource(path) {
   910  		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   911  	}
   912  	p.buf.WriteRune('\n')
   913  	p.buf.WriteString(strings.Repeat(" ", indent+4))
   914  	p.buf.WriteString("# At least one attribute in this block is (or was) sensitive,\n")
   915  	p.buf.WriteString(strings.Repeat(" ", indent+4))
   916  	p.buf.WriteString("# so its contents will not be displayed.")
   917  	p.buf.WriteRune('\n')
   918  	p.buf.WriteString(strings.Repeat(" ", indent+2))
   919  	p.buf.WriteString("}")
   920  }
   921  
   922  func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) bool {
   923  	if action == plans.NoOp && !p.verbose {
   924  		return true
   925  	}
   926  
   927  	p.buf.WriteString("\n")
   928  	p.buf.WriteString(strings.Repeat(" ", indent))
   929  	p.writeActionSymbol(action)
   930  
   931  	if label != nil {
   932  		fmt.Fprintf(p.buf, "%s %q {", name, *label)
   933  	} else {
   934  		fmt.Fprintf(p.buf, "%s {", name)
   935  	}
   936  
   937  	if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) {
   938  		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
   939  	}
   940  
   941  	result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path)
   942  	if result.bodyWritten {
   943  		p.buf.WriteString("\n")
   944  		p.buf.WriteString(strings.Repeat(" ", indent+2))
   945  	}
   946  	p.buf.WriteString("}")
   947  
   948  	return false
   949  }
   950  
   951  func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) {
   952  	// Could check specifically for the sensitivity marker
   953  	if val.HasMark(marks.Sensitive) {
   954  		p.buf.WriteString("(sensitive)")
   955  		return
   956  	}
   957  
   958  	if !val.IsKnown() {
   959  		p.buf.WriteString("(known after apply)")
   960  		return
   961  	}
   962  	if val.IsNull() {
   963  		p.buf.WriteString(p.color.Color("[dark_gray]null[reset]"))
   964  		return
   965  	}
   966  
   967  	ty := val.Type()
   968  
   969  	switch {
   970  	case ty.IsPrimitiveType():
   971  		switch ty {
   972  		case cty.String:
   973  			{
   974  				// Special behavior for JSON strings containing array or object
   975  				src := []byte(val.AsString())
   976  				ty, err := ctyjson.ImpliedType(src)
   977  				// check for the special case of "null", which decodes to nil,
   978  				// and just allow it to be printed out directly
   979  				if err == nil && !ty.IsPrimitiveType() && strings.TrimSpace(val.AsString()) != "null" {
   980  					jv, err := ctyjson.Unmarshal(src, ty)
   981  					if err == nil {
   982  						p.buf.WriteString("jsonencode(")
   983  						if jv.LengthInt() == 0 {
   984  							p.writeValue(jv, action, 0)
   985  						} else {
   986  							p.buf.WriteByte('\n')
   987  							p.buf.WriteString(strings.Repeat(" ", indent+4))
   988  							p.writeValue(jv, action, indent+4)
   989  							p.buf.WriteByte('\n')
   990  							p.buf.WriteString(strings.Repeat(" ", indent))
   991  						}
   992  						p.buf.WriteByte(')')
   993  						break // don't *also* do the normal behavior below
   994  					}
   995  				}
   996  			}
   997  
   998  			if strings.Contains(val.AsString(), "\n") {
   999  				// It's a multi-line string, so we want to use the multi-line
  1000  				// rendering so it'll be readable. Rather than re-implement
  1001  				// that here, we'll just re-use the multi-line string diff
  1002  				// printer with no changes, which ends up producing the
  1003  				// result we want here.
  1004  				// The path argument is nil because we don't track path
  1005  				// information into strings and we know that a string can't
  1006  				// have any indices or attributes that might need to be marked
  1007  				// as (requires replacement), which is what that argument is for.
  1008  				p.writeValueDiff(val, val, indent, nil)
  1009  				break
  1010  			}
  1011  
  1012  			fmt.Fprintf(p.buf, "%q", val.AsString())
  1013  		case cty.Bool:
  1014  			if val.True() {
  1015  				p.buf.WriteString("true")
  1016  			} else {
  1017  				p.buf.WriteString("false")
  1018  			}
  1019  		case cty.Number:
  1020  			bf := val.AsBigFloat()
  1021  			p.buf.WriteString(bf.Text('f', -1))
  1022  		default:
  1023  			// should never happen, since the above is exhaustive
  1024  			fmt.Fprintf(p.buf, "%#v", val)
  1025  		}
  1026  	case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
  1027  		p.buf.WriteString("[")
  1028  
  1029  		it := val.ElementIterator()
  1030  		for it.Next() {
  1031  			_, val := it.Element()
  1032  
  1033  			p.buf.WriteString("\n")
  1034  			p.buf.WriteString(strings.Repeat(" ", indent+2))
  1035  			p.writeActionSymbol(action)
  1036  			p.writeValue(val, action, indent+4)
  1037  			p.buf.WriteString(",")
  1038  		}
  1039  
  1040  		if val.LengthInt() > 0 {
  1041  			p.buf.WriteString("\n")
  1042  			p.buf.WriteString(strings.Repeat(" ", indent))
  1043  		}
  1044  		p.buf.WriteString("]")
  1045  	case ty.IsMapType():
  1046  		p.buf.WriteString("{")
  1047  
  1048  		keyLen := 0
  1049  		for it := val.ElementIterator(); it.Next(); {
  1050  			key, _ := it.Element()
  1051  			if keyStr := key.AsString(); len(keyStr) > keyLen {
  1052  				keyLen = len(keyStr)
  1053  			}
  1054  		}
  1055  
  1056  		for it := val.ElementIterator(); it.Next(); {
  1057  			key, val := it.Element()
  1058  
  1059  			p.buf.WriteString("\n")
  1060  			p.buf.WriteString(strings.Repeat(" ", indent+2))
  1061  			p.writeActionSymbol(action)
  1062  			p.writeValue(key, action, indent+4)
  1063  			p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString())))
  1064  			p.buf.WriteString(" = ")
  1065  			p.writeValue(val, action, indent+4)
  1066  		}
  1067  
  1068  		if val.LengthInt() > 0 {
  1069  			p.buf.WriteString("\n")
  1070  			p.buf.WriteString(strings.Repeat(" ", indent))
  1071  		}
  1072  		p.buf.WriteString("}")
  1073  	case ty.IsObjectType():
  1074  		p.buf.WriteString("{")
  1075  
  1076  		atys := ty.AttributeTypes()
  1077  		attrNames := make([]string, 0, len(atys))
  1078  		nameLen := 0
  1079  		for attrName := range atys {
  1080  			attrNames = append(attrNames, attrName)
  1081  			if len(attrName) > nameLen {
  1082  				nameLen = len(attrName)
  1083  			}
  1084  		}
  1085  		sort.Strings(attrNames)
  1086  
  1087  		for _, attrName := range attrNames {
  1088  			val := val.GetAttr(attrName)
  1089  
  1090  			p.buf.WriteString("\n")
  1091  			p.buf.WriteString(strings.Repeat(" ", indent+2))
  1092  			p.writeActionSymbol(action)
  1093  			p.buf.WriteString(attrName)
  1094  			p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName)))
  1095  			p.buf.WriteString(" = ")
  1096  			p.writeValue(val, action, indent+4)
  1097  		}
  1098  
  1099  		if len(attrNames) > 0 {
  1100  			p.buf.WriteString("\n")
  1101  			p.buf.WriteString(strings.Repeat(" ", indent))
  1102  		}
  1103  		p.buf.WriteString("}")
  1104  	}
  1105  }
  1106  
  1107  func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) {
  1108  	ty := old.Type()
  1109  	typesEqual := ctyTypesEqual(ty, new.Type())
  1110  
  1111  	// We have some specialized diff implementations for certain complex
  1112  	// values where it's useful to see a visualization of the diff of
  1113  	// the nested elements rather than just showing the entire old and
  1114  	// new values verbatim.
  1115  	// However, these specialized implementations can apply only if both
  1116  	// values are known and non-null.
  1117  	if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual {
  1118  		if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) {
  1119  			p.buf.WriteString("(sensitive)")
  1120  			if p.pathForcesNewResource(path) {
  1121  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1122  			}
  1123  			return
  1124  		}
  1125  
  1126  		switch {
  1127  		case ty == cty.String:
  1128  			// We have special behavior for both multi-line strings in general
  1129  			// and for strings that can parse as JSON. For the JSON handling
  1130  			// to apply, both old and new must be valid JSON.
  1131  			// For single-line strings that don't parse as JSON we just fall
  1132  			// out of this switch block and do the default old -> new rendering.
  1133  			oldS := old.AsString()
  1134  			newS := new.AsString()
  1135  
  1136  			{
  1137  				// Special behavior for JSON strings containing object or
  1138  				// list values.
  1139  				oldBytes := []byte(oldS)
  1140  				newBytes := []byte(newS)
  1141  				oldType, oldErr := ctyjson.ImpliedType(oldBytes)
  1142  				newType, newErr := ctyjson.ImpliedType(newBytes)
  1143  				if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) {
  1144  					oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType)
  1145  					newJV, newErr := ctyjson.Unmarshal(newBytes, newType)
  1146  					if oldErr == nil && newErr == nil {
  1147  						if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace
  1148  							p.buf.WriteString("jsonencode(")
  1149  							p.buf.WriteByte('\n')
  1150  							p.buf.WriteString(strings.Repeat(" ", indent+2))
  1151  							p.writeActionSymbol(plans.Update)
  1152  							p.writeValueDiff(oldJV, newJV, indent+4, path)
  1153  							p.buf.WriteByte('\n')
  1154  							p.buf.WriteString(strings.Repeat(" ", indent))
  1155  							p.buf.WriteByte(')')
  1156  						} else {
  1157  							// if they differ only in insignificant whitespace
  1158  							// then we'll note that but still expand out the
  1159  							// effective value.
  1160  							if p.pathForcesNewResource(path) {
  1161  								p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]"))
  1162  							} else {
  1163  								p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]"))
  1164  							}
  1165  							p.buf.WriteByte('\n')
  1166  							p.buf.WriteString(strings.Repeat(" ", indent+4))
  1167  							p.writeValue(oldJV, plans.NoOp, indent+4)
  1168  							p.buf.WriteByte('\n')
  1169  							p.buf.WriteString(strings.Repeat(" ", indent))
  1170  							p.buf.WriteByte(')')
  1171  						}
  1172  						return
  1173  					}
  1174  				}
  1175  			}
  1176  
  1177  			if !strings.Contains(oldS, "\n") && !strings.Contains(newS, "\n") {
  1178  				break
  1179  			}
  1180  
  1181  			p.buf.WriteString("<<-EOT")
  1182  			if p.pathForcesNewResource(path) {
  1183  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1184  			}
  1185  			p.buf.WriteString("\n")
  1186  
  1187  			var oldLines, newLines []cty.Value
  1188  			{
  1189  				r := strings.NewReader(oldS)
  1190  				sc := bufio.NewScanner(r)
  1191  				for sc.Scan() {
  1192  					oldLines = append(oldLines, cty.StringVal(sc.Text()))
  1193  				}
  1194  			}
  1195  			{
  1196  				r := strings.NewReader(newS)
  1197  				sc := bufio.NewScanner(r)
  1198  				for sc.Scan() {
  1199  					newLines = append(newLines, cty.StringVal(sc.Text()))
  1200  				}
  1201  			}
  1202  
  1203  			// Optimization for strings which are exactly equal: just print
  1204  			// directly without calculating the sequence diff. This makes a
  1205  			// significant difference when this code path is reached via a
  1206  			// writeValue call with a large multi-line string.
  1207  			if oldS == newS {
  1208  				for _, line := range newLines {
  1209  					p.buf.WriteString(strings.Repeat(" ", indent+4))
  1210  					p.buf.WriteString(line.AsString())
  1211  					p.buf.WriteString("\n")
  1212  				}
  1213  			} else {
  1214  				diffLines := ctySequenceDiff(oldLines, newLines)
  1215  				for _, diffLine := range diffLines {
  1216  					p.buf.WriteString(strings.Repeat(" ", indent+2))
  1217  					p.writeActionSymbol(diffLine.Action)
  1218  
  1219  					switch diffLine.Action {
  1220  					case plans.NoOp, plans.Delete:
  1221  						p.buf.WriteString(diffLine.Before.AsString())
  1222  					case plans.Create:
  1223  						p.buf.WriteString(diffLine.After.AsString())
  1224  					default:
  1225  						// Should never happen since the above covers all
  1226  						// actions that ctySequenceDiff can return for strings
  1227  						p.buf.WriteString(diffLine.After.AsString())
  1228  
  1229  					}
  1230  					p.buf.WriteString("\n")
  1231  				}
  1232  			}
  1233  
  1234  			p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol
  1235  			p.buf.WriteString("EOT")
  1236  
  1237  			return
  1238  
  1239  		case ty.IsSetType():
  1240  			p.buf.WriteString("[")
  1241  			if p.pathForcesNewResource(path) {
  1242  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1243  			}
  1244  			p.buf.WriteString("\n")
  1245  
  1246  			var addedVals, removedVals, allVals []cty.Value
  1247  			for it := old.ElementIterator(); it.Next(); {
  1248  				_, val := it.Element()
  1249  				allVals = append(allVals, val)
  1250  				if new.HasElement(val).False() {
  1251  					removedVals = append(removedVals, val)
  1252  				}
  1253  			}
  1254  			for it := new.ElementIterator(); it.Next(); {
  1255  				_, val := it.Element()
  1256  				allVals = append(allVals, val)
  1257  				if val.IsKnown() && old.HasElement(val).False() {
  1258  					addedVals = append(addedVals, val)
  1259  				}
  1260  			}
  1261  
  1262  			var all, added, removed cty.Value
  1263  			if len(allVals) > 0 {
  1264  				all = cty.SetVal(allVals)
  1265  			} else {
  1266  				all = cty.SetValEmpty(ty.ElementType())
  1267  			}
  1268  			if len(addedVals) > 0 {
  1269  				added = cty.SetVal(addedVals)
  1270  			} else {
  1271  				added = cty.SetValEmpty(ty.ElementType())
  1272  			}
  1273  			if len(removedVals) > 0 {
  1274  				removed = cty.SetVal(removedVals)
  1275  			} else {
  1276  				removed = cty.SetValEmpty(ty.ElementType())
  1277  			}
  1278  
  1279  			suppressedElements := 0
  1280  			for it := all.ElementIterator(); it.Next(); {
  1281  				_, val := it.Element()
  1282  
  1283  				var action plans.Action
  1284  				switch {
  1285  				case !val.IsKnown():
  1286  					action = plans.Update
  1287  				case added.HasElement(val).True():
  1288  					action = plans.Create
  1289  				case removed.HasElement(val).True():
  1290  					action = plans.Delete
  1291  				default:
  1292  					action = plans.NoOp
  1293  				}
  1294  
  1295  				if action == plans.NoOp && !p.verbose {
  1296  					suppressedElements++
  1297  					continue
  1298  				}
  1299  
  1300  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1301  				p.writeActionSymbol(action)
  1302  				p.writeValue(val, action, indent+4)
  1303  				p.buf.WriteString(",\n")
  1304  			}
  1305  
  1306  			if suppressedElements > 0 {
  1307  				p.writeActionSymbol(plans.NoOp)
  1308  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1309  				noun := "elements"
  1310  				if suppressedElements == 1 {
  1311  					noun = "element"
  1312  				}
  1313  				p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
  1314  				p.buf.WriteString("\n")
  1315  			}
  1316  
  1317  			p.buf.WriteString(strings.Repeat(" ", indent))
  1318  			p.buf.WriteString("]")
  1319  			return
  1320  		case ty.IsListType() || ty.IsTupleType():
  1321  			p.buf.WriteString("[")
  1322  			if p.pathForcesNewResource(path) {
  1323  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1324  			}
  1325  			p.buf.WriteString("\n")
  1326  
  1327  			elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice())
  1328  
  1329  			// Maintain a stack of suppressed lines in the diff for later
  1330  			// display or elision
  1331  			var suppressedElements []*plans.Change
  1332  			var changeShown bool
  1333  
  1334  			for i := 0; i < len(elemDiffs); i++ {
  1335  				if !p.verbose {
  1336  					for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp {
  1337  						suppressedElements = append(suppressedElements, elemDiffs[i])
  1338  						i++
  1339  					}
  1340  				}
  1341  
  1342  				// If we have some suppressed elements on the stack…
  1343  				if len(suppressedElements) > 0 {
  1344  					// If we've just rendered a change, display the first
  1345  					// element in the stack as context
  1346  					if changeShown {
  1347  						elemDiff := suppressedElements[0]
  1348  						p.buf.WriteString(strings.Repeat(" ", indent+4))
  1349  						p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
  1350  						p.buf.WriteString(",\n")
  1351  						suppressedElements = suppressedElements[1:]
  1352  					}
  1353  
  1354  					hidden := len(suppressedElements)
  1355  
  1356  					// If we're not yet at the end of the list, capture the
  1357  					// last element on the stack as context for the upcoming
  1358  					// change to be rendered
  1359  					var nextContextDiff *plans.Change
  1360  					if hidden > 0 && i < len(elemDiffs) {
  1361  						hidden--
  1362  						nextContextDiff = suppressedElements[hidden]
  1363  					}
  1364  
  1365  					// If there are still hidden elements, show an elision
  1366  					// statement counting them
  1367  					if hidden > 0 {
  1368  						p.writeActionSymbol(plans.NoOp)
  1369  						p.buf.WriteString(strings.Repeat(" ", indent+2))
  1370  						noun := "elements"
  1371  						if hidden == 1 {
  1372  							noun = "element"
  1373  						}
  1374  						p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), hidden, noun))
  1375  						p.buf.WriteString("\n")
  1376  					}
  1377  
  1378  					// Display the next context diff if it was captured above
  1379  					if nextContextDiff != nil {
  1380  						p.buf.WriteString(strings.Repeat(" ", indent+4))
  1381  						p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4)
  1382  						p.buf.WriteString(",\n")
  1383  					}
  1384  
  1385  					// Suppressed elements have now been handled so clear them again
  1386  					suppressedElements = nil
  1387  				}
  1388  
  1389  				if i >= len(elemDiffs) {
  1390  					break
  1391  				}
  1392  
  1393  				elemDiff := elemDiffs[i]
  1394  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1395  				p.writeActionSymbol(elemDiff.Action)
  1396  				switch elemDiff.Action {
  1397  				case plans.NoOp, plans.Delete:
  1398  					p.writeValue(elemDiff.Before, elemDiff.Action, indent+4)
  1399  				case plans.Update:
  1400  					p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path)
  1401  				case plans.Create:
  1402  					p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
  1403  				default:
  1404  					// Should never happen since the above covers all
  1405  					// actions that ctySequenceDiff can return.
  1406  					p.writeValue(elemDiff.After, elemDiff.Action, indent+4)
  1407  				}
  1408  
  1409  				p.buf.WriteString(",\n")
  1410  				changeShown = true
  1411  			}
  1412  
  1413  			p.buf.WriteString(strings.Repeat(" ", indent))
  1414  			p.buf.WriteString("]")
  1415  
  1416  			return
  1417  
  1418  		case ty.IsMapType():
  1419  			p.buf.WriteString("{")
  1420  			if p.pathForcesNewResource(path) {
  1421  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1422  			}
  1423  			p.buf.WriteString("\n")
  1424  
  1425  			var allKeys []string
  1426  			keyLen := 0
  1427  			for it := old.ElementIterator(); it.Next(); {
  1428  				k, _ := it.Element()
  1429  				keyStr := k.AsString()
  1430  				allKeys = append(allKeys, keyStr)
  1431  				if len(keyStr) > keyLen {
  1432  					keyLen = len(keyStr)
  1433  				}
  1434  			}
  1435  			for it := new.ElementIterator(); it.Next(); {
  1436  				k, _ := it.Element()
  1437  				keyStr := k.AsString()
  1438  				allKeys = append(allKeys, keyStr)
  1439  				if len(keyStr) > keyLen {
  1440  					keyLen = len(keyStr)
  1441  				}
  1442  			}
  1443  
  1444  			sort.Strings(allKeys)
  1445  
  1446  			suppressedElements := 0
  1447  			lastK := ""
  1448  			for i, k := range allKeys {
  1449  				if i > 0 && lastK == k {
  1450  					continue // skip duplicates (list is sorted)
  1451  				}
  1452  				lastK = k
  1453  
  1454  				kV := cty.StringVal(k)
  1455  				var action plans.Action
  1456  				if old.HasIndex(kV).False() {
  1457  					action = plans.Create
  1458  				} else if new.HasIndex(kV).False() {
  1459  					action = plans.Delete
  1460  				}
  1461  
  1462  				if old.HasIndex(kV).True() && new.HasIndex(kV).True() {
  1463  					if ctyEqualValueAndMarks(old.Index(kV), new.Index(kV)) {
  1464  						action = plans.NoOp
  1465  					} else {
  1466  						action = plans.Update
  1467  					}
  1468  				}
  1469  
  1470  				if action == plans.NoOp && !p.verbose {
  1471  					suppressedElements++
  1472  					continue
  1473  				}
  1474  
  1475  				path := append(path, cty.IndexStep{Key: kV})
  1476  
  1477  				oldV := old.Index(kV)
  1478  				newV := new.Index(kV)
  1479  				p.writeSensitivityWarning(oldV, newV, indent+2, action, false)
  1480  
  1481  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1482  				p.writeActionSymbol(action)
  1483  				p.writeValue(kV, action, indent+4)
  1484  				p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
  1485  				p.buf.WriteString(" = ")
  1486  				switch action {
  1487  				case plans.Create, plans.NoOp:
  1488  					v := new.Index(kV)
  1489  					if v.HasMark(marks.Sensitive) {
  1490  						p.buf.WriteString("(sensitive)")
  1491  					} else {
  1492  						p.writeValue(v, action, indent+4)
  1493  					}
  1494  				case plans.Delete:
  1495  					oldV := old.Index(kV)
  1496  					newV := cty.NullVal(oldV.Type())
  1497  					p.writeValueDiff(oldV, newV, indent+4, path)
  1498  				default:
  1499  					if oldV.HasMark(marks.Sensitive) || newV.HasMark(marks.Sensitive) {
  1500  						p.buf.WriteString("(sensitive)")
  1501  					} else {
  1502  						p.writeValueDiff(oldV, newV, indent+4, path)
  1503  					}
  1504  				}
  1505  
  1506  				p.buf.WriteByte('\n')
  1507  			}
  1508  
  1509  			if suppressedElements > 0 {
  1510  				p.writeActionSymbol(plans.NoOp)
  1511  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1512  				noun := "elements"
  1513  				if suppressedElements == 1 {
  1514  					noun = "element"
  1515  				}
  1516  				p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
  1517  				p.buf.WriteString("\n")
  1518  			}
  1519  
  1520  			p.buf.WriteString(strings.Repeat(" ", indent))
  1521  			p.buf.WriteString("}")
  1522  
  1523  			return
  1524  		case ty.IsObjectType():
  1525  			p.buf.WriteString("{")
  1526  			p.buf.WriteString("\n")
  1527  
  1528  			forcesNewResource := p.pathForcesNewResource(path)
  1529  
  1530  			var allKeys []string
  1531  			keyLen := 0
  1532  			for it := old.ElementIterator(); it.Next(); {
  1533  				k, _ := it.Element()
  1534  				keyStr := k.AsString()
  1535  				allKeys = append(allKeys, keyStr)
  1536  				if len(keyStr) > keyLen {
  1537  					keyLen = len(keyStr)
  1538  				}
  1539  			}
  1540  			for it := new.ElementIterator(); it.Next(); {
  1541  				k, _ := it.Element()
  1542  				keyStr := k.AsString()
  1543  				allKeys = append(allKeys, keyStr)
  1544  				if len(keyStr) > keyLen {
  1545  					keyLen = len(keyStr)
  1546  				}
  1547  			}
  1548  
  1549  			sort.Strings(allKeys)
  1550  
  1551  			suppressedElements := 0
  1552  			lastK := ""
  1553  			for i, k := range allKeys {
  1554  				if i > 0 && lastK == k {
  1555  					continue // skip duplicates (list is sorted)
  1556  				}
  1557  				lastK = k
  1558  
  1559  				kV := k
  1560  				var action plans.Action
  1561  				if !old.Type().HasAttribute(kV) {
  1562  					action = plans.Create
  1563  				} else if !new.Type().HasAttribute(kV) {
  1564  					action = plans.Delete
  1565  				} else if ctyEqualValueAndMarks(old.GetAttr(kV), new.GetAttr(kV)) {
  1566  					action = plans.NoOp
  1567  				} else {
  1568  					action = plans.Update
  1569  				}
  1570  
  1571  				if action == plans.NoOp && !p.verbose {
  1572  					suppressedElements++
  1573  					continue
  1574  				}
  1575  
  1576  				path := append(path, cty.GetAttrStep{Name: kV})
  1577  
  1578  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1579  				p.writeActionSymbol(action)
  1580  				p.buf.WriteString(k)
  1581  				p.buf.WriteString(strings.Repeat(" ", keyLen-len(k)))
  1582  				p.buf.WriteString(" = ")
  1583  
  1584  				switch action {
  1585  				case plans.Create, plans.NoOp:
  1586  					v := new.GetAttr(kV)
  1587  					p.writeValue(v, action, indent+4)
  1588  				case plans.Delete:
  1589  					oldV := old.GetAttr(kV)
  1590  					newV := cty.NullVal(oldV.Type())
  1591  					p.writeValueDiff(oldV, newV, indent+4, path)
  1592  				default:
  1593  					oldV := old.GetAttr(kV)
  1594  					newV := new.GetAttr(kV)
  1595  					p.writeValueDiff(oldV, newV, indent+4, path)
  1596  				}
  1597  
  1598  				p.buf.WriteString("\n")
  1599  			}
  1600  
  1601  			if suppressedElements > 0 {
  1602  				p.writeActionSymbol(plans.NoOp)
  1603  				p.buf.WriteString(strings.Repeat(" ", indent+2))
  1604  				noun := "elements"
  1605  				if suppressedElements == 1 {
  1606  					noun = "element"
  1607  				}
  1608  				p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun))
  1609  				p.buf.WriteString("\n")
  1610  			}
  1611  
  1612  			p.buf.WriteString(strings.Repeat(" ", indent))
  1613  			p.buf.WriteString("}")
  1614  
  1615  			if forcesNewResource {
  1616  				p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1617  			}
  1618  			return
  1619  		}
  1620  	}
  1621  
  1622  	// In all other cases, we just show the new and old values as-is
  1623  	p.writeValue(old, plans.Delete, indent)
  1624  	if new.IsNull() {
  1625  		p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] "))
  1626  	} else {
  1627  		p.buf.WriteString(p.color.Color(" [yellow]->[reset] "))
  1628  	}
  1629  
  1630  	p.writeValue(new, plans.Create, indent)
  1631  	if p.pathForcesNewResource(path) {
  1632  		p.buf.WriteString(p.color.Color(forcesNewResourceCaption))
  1633  	}
  1634  }
  1635  
  1636  // writeActionSymbol writes a symbol to represent the given action, followed
  1637  // by a space.
  1638  //
  1639  // It only supports the actions that can be represented with a single character:
  1640  // Create, Delete, Update and NoAction.
  1641  func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) {
  1642  	switch action {
  1643  	case plans.Create:
  1644  		p.buf.WriteString(p.color.Color("[green]+[reset] "))
  1645  	case plans.Delete:
  1646  		p.buf.WriteString(p.color.Color("[red]-[reset] "))
  1647  	case plans.Update:
  1648  		p.buf.WriteString(p.color.Color("[yellow]~[reset] "))
  1649  	case plans.NoOp:
  1650  		p.buf.WriteString("  ")
  1651  	default:
  1652  		// Should never happen
  1653  		p.buf.WriteString(p.color.Color("? "))
  1654  	}
  1655  }
  1656  
  1657  func (p *blockBodyDiffPrinter) writeSensitivityWarning(old, new cty.Value, indent int, action plans.Action, isBlock bool) {
  1658  	// Dont' show this warning for create or delete
  1659  	if action == plans.Create || action == plans.Delete {
  1660  		return
  1661  	}
  1662  
  1663  	// Customize the warning based on if it is an attribute or block
  1664  	diffType := "attribute value"
  1665  	if isBlock {
  1666  		diffType = "block"
  1667  	}
  1668  
  1669  	// If only attribute sensitivity is changing, clarify that the value is unchanged
  1670  	var valueUnchangedSuffix string
  1671  	if !isBlock {
  1672  		oldUnmarked, _ := old.UnmarkDeep()
  1673  		newUnmarked, _ := new.UnmarkDeep()
  1674  		if oldUnmarked.RawEquals(newUnmarked) {
  1675  			valueUnchangedSuffix = " The value is unchanged."
  1676  		}
  1677  	}
  1678  
  1679  	if new.HasMark(marks.Sensitive) && !old.HasMark(marks.Sensitive) {
  1680  		p.buf.WriteString(strings.Repeat(" ", indent))
  1681  		p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will be marked as sensitive and will not\n"), diffType))
  1682  		p.buf.WriteString(strings.Repeat(" ", indent))
  1683  		p.buf.WriteString(fmt.Sprintf("# display in UI output after applying this change.%s\n", valueUnchangedSuffix))
  1684  	}
  1685  
  1686  	// Note if changing this attribute will change its sensitivity
  1687  	if old.HasMark(marks.Sensitive) && !new.HasMark(marks.Sensitive) {
  1688  		p.buf.WriteString(strings.Repeat(" ", indent))
  1689  		p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will no longer be marked as sensitive\n"), diffType))
  1690  		p.buf.WriteString(strings.Repeat(" ", indent))
  1691  		p.buf.WriteString(fmt.Sprintf("# after applying this change.%s\n", valueUnchangedSuffix))
  1692  	}
  1693  }
  1694  
  1695  func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool {
  1696  	if !p.action.IsReplace() || p.requiredReplace.Empty() {
  1697  		// "requiredReplace" only applies when the instance is being replaced,
  1698  		// and we should only inspect that set if it is not empty
  1699  		return false
  1700  	}
  1701  	return p.requiredReplace.Has(path)
  1702  }
  1703  
  1704  func ctyEmptyString(value cty.Value) bool {
  1705  	if !value.IsNull() && value.IsKnown() {
  1706  		valueType := value.Type()
  1707  		if valueType == cty.String && value.AsString() == "" {
  1708  			return true
  1709  		}
  1710  	}
  1711  	return false
  1712  }
  1713  
  1714  func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value {
  1715  	attrType := val.Type().AttributeType(name)
  1716  
  1717  	if val.IsNull() {
  1718  		return cty.NullVal(attrType)
  1719  	}
  1720  
  1721  	// We treat "" as null here
  1722  	// as existing SDK doesn't support null yet.
  1723  	// This allows us to avoid spurious diffs
  1724  	// until we introduce null to the SDK.
  1725  	attrValue := val.GetAttr(name)
  1726  	// If the value is marked, the ctyEmptyString function will fail
  1727  	if !val.ContainsMarked() && ctyEmptyString(attrValue) {
  1728  		return cty.NullVal(attrType)
  1729  	}
  1730  
  1731  	return attrValue
  1732  }
  1733  
  1734  func ctyCollectionValues(val cty.Value) []cty.Value {
  1735  	if !val.IsKnown() || val.IsNull() {
  1736  		return nil
  1737  	}
  1738  
  1739  	ret := make([]cty.Value, 0, val.LengthInt())
  1740  	for it := val.ElementIterator(); it.Next(); {
  1741  		_, value := it.Element()
  1742  		ret = append(ret, value)
  1743  	}
  1744  	return ret
  1745  }
  1746  
  1747  // ctySequenceDiff returns differences between given sequences of cty.Value(s)
  1748  // in the form of Create, Delete, or Update actions (for objects).
  1749  func ctySequenceDiff(old, new []cty.Value) []*plans.Change {
  1750  	var ret []*plans.Change
  1751  	lcs := objchange.LongestCommonSubsequence(old, new)
  1752  	var oldI, newI, lcsI int
  1753  	for oldI < len(old) || newI < len(new) || lcsI < len(lcs) {
  1754  		// We first process items in the old and new sequences which are not
  1755  		// equal to the current common sequence item.  Old items are marked as
  1756  		// deletions, and new items are marked as additions.
  1757  		//
  1758  		// There is an exception for deleted & created object items, which we
  1759  		// try to render as updates where that makes sense.
  1760  		for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) {
  1761  			// Render this as an object update if all of these are true:
  1762  			//
  1763  			// - the current old item is an object;
  1764  			// - there's a current new item which is also an object;
  1765  			// - either there are no common items left, or the current new item
  1766  			//   doesn't equal the current common item.
  1767  			//
  1768  			// Why do we need the the last clause? If we have current items in all
  1769  			// three sequences, and the current new item is equal to a common item,
  1770  			// then we should just need to advance the old item list and we'll
  1771  			// eventually find a common item matching both old and new.
  1772  			//
  1773  			// This combination of conditions allows us to render an object update
  1774  			// diff instead of a combination of delete old & create new.
  1775  			isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI]))
  1776  			if isObjectDiff {
  1777  				ret = append(ret, &plans.Change{
  1778  					Action: plans.Update,
  1779  					Before: old[oldI],
  1780  					After:  new[newI],
  1781  				})
  1782  				oldI++
  1783  				newI++ // we also consume the next "new" in this case
  1784  				continue
  1785  			}
  1786  
  1787  			// Otherwise, this item is not part of the common sequence, so
  1788  			// render as a deletion.
  1789  			ret = append(ret, &plans.Change{
  1790  				Action: plans.Delete,
  1791  				Before: old[oldI],
  1792  				After:  cty.NullVal(old[oldI].Type()),
  1793  			})
  1794  			oldI++
  1795  		}
  1796  		for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) {
  1797  			ret = append(ret, &plans.Change{
  1798  				Action: plans.Create,
  1799  				Before: cty.NullVal(new[newI].Type()),
  1800  				After:  new[newI],
  1801  			})
  1802  			newI++
  1803  		}
  1804  
  1805  		// When we've exhausted the old & new sequences of items which are not
  1806  		// in the common subsequence, we render a common item and continue.
  1807  		if lcsI < len(lcs) {
  1808  			ret = append(ret, &plans.Change{
  1809  				Action: plans.NoOp,
  1810  				Before: lcs[lcsI],
  1811  				After:  lcs[lcsI],
  1812  			})
  1813  
  1814  			// All of our indexes advance together now, since the line
  1815  			// is common to all three sequences.
  1816  			lcsI++
  1817  			oldI++
  1818  			newI++
  1819  		}
  1820  	}
  1821  	return ret
  1822  }
  1823  
  1824  // ctyEqualValueAndMarks checks equality of two possibly-marked values,
  1825  // considering partially-unknown values and equal values with different marks
  1826  // as inequal
  1827  func ctyEqualWithUnknown(old, new cty.Value) bool {
  1828  	if !old.IsWhollyKnown() || !new.IsWhollyKnown() {
  1829  		return false
  1830  	}
  1831  	return ctyEqualValueAndMarks(old, new)
  1832  }
  1833  
  1834  // ctyEqualValueAndMarks checks equality of two possibly-marked values,
  1835  // considering equal values with different marks as inequal
  1836  func ctyEqualValueAndMarks(old, new cty.Value) bool {
  1837  	oldUnmarked, oldMarks := old.UnmarkDeep()
  1838  	newUnmarked, newMarks := new.UnmarkDeep()
  1839  	sameValue := oldUnmarked.Equals(newUnmarked)
  1840  	return sameValue.IsKnown() && sameValue.True() && oldMarks.Equal(newMarks)
  1841  }
  1842  
  1843  // ctyTypesEqual checks equality of two types more loosely
  1844  // by avoiding checks of object/tuple elements
  1845  // as we render differences on element-by-element basis anyway
  1846  func ctyTypesEqual(oldT, newT cty.Type) bool {
  1847  	if oldT.IsObjectType() && newT.IsObjectType() {
  1848  		return true
  1849  	}
  1850  	if oldT.IsTupleType() && newT.IsTupleType() {
  1851  		return true
  1852  	}
  1853  	return oldT.Equals(newT)
  1854  }
  1855  
  1856  func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path {
  1857  	if cap(path)-len(path) >= minExtra {
  1858  		return path
  1859  	}
  1860  	newCap := cap(path) * 2
  1861  	if newCap < (len(path) + minExtra) {
  1862  		newCap = len(path) + minExtra
  1863  	}
  1864  	newPath := make(cty.Path, len(path), newCap)
  1865  	copy(newPath, path)
  1866  	return newPath
  1867  }
  1868  
  1869  // ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil
  1870  // or returns an empty value of a suitable type to serve as a placeholder for it.
  1871  //
  1872  // In particular, this function handles the special situation where a "list" is
  1873  // actually represented as a tuple type where nested blocks contain
  1874  // dynamically-typed values.
  1875  func ctyNullBlockListAsEmpty(in cty.Value) cty.Value {
  1876  	if !in.IsNull() {
  1877  		return in
  1878  	}
  1879  	if ty := in.Type(); ty.IsListType() {
  1880  		return cty.ListValEmpty(ty.ElementType())
  1881  	}
  1882  	return cty.EmptyTupleVal // must need a tuple, then
  1883  }
  1884  
  1885  // ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil
  1886  // or returns an empty value of a suitable type to serve as a placeholder for it.
  1887  //
  1888  // In particular, this function handles the special situation where a "map" is
  1889  // actually represented as an object type where nested blocks contain
  1890  // dynamically-typed values.
  1891  func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value {
  1892  	if !in.IsNull() {
  1893  		return in
  1894  	}
  1895  	if ty := in.Type(); ty.IsMapType() {
  1896  		return cty.MapValEmpty(ty.ElementType())
  1897  	}
  1898  	return cty.EmptyObjectVal // must need an object, then
  1899  }
  1900  
  1901  // ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil
  1902  // or returns an empty value of a suitable type to serve as a placeholder for it.
  1903  func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value {
  1904  	if !in.IsNull() {
  1905  		return in
  1906  	}
  1907  	// Dynamically-typed attributes are not supported inside blocks backed by
  1908  	// sets, so our result here is always a set.
  1909  	return cty.SetValEmpty(in.Type().ElementType())
  1910  }
  1911  
  1912  // DiffActionSymbol returns a string that, once passed through a
  1913  // colorstring.Colorize, will produce a result that can be written
  1914  // to a terminal to produce a symbol made of three printable
  1915  // characters, possibly interspersed with VT100 color codes.
  1916  func DiffActionSymbol(action plans.Action) string {
  1917  	switch action {
  1918  	case plans.DeleteThenCreate:
  1919  		return "[red]-[reset]/[green]+[reset]"
  1920  	case plans.CreateThenDelete:
  1921  		return "[green]+[reset]/[red]-[reset]"
  1922  	case plans.Create:
  1923  		return "  [green]+[reset]"
  1924  	case plans.Delete:
  1925  		return "  [red]-[reset]"
  1926  	case plans.Read:
  1927  		return " [cyan]<=[reset]"
  1928  	case plans.Update:
  1929  		return "  [yellow]~[reset]"
  1930  	default:
  1931  		return "  ?"
  1932  	}
  1933  }
  1934  
  1935  // Extremely coarse heuristic for determining whether or not a given attribute
  1936  // name is important for identifying a resource. In the future, this may be
  1937  // replaced by a flag in the schema, but for now this is likely to be good
  1938  // enough.
  1939  func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool {
  1940  	return name == "id" || name == "tags" || name == "name"
  1941  }
  1942  
  1943  func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) {
  1944  	if skipped > 0 {
  1945  		noun := "attributes"
  1946  		if skipped == 1 {
  1947  			noun = "attribute"
  1948  		}
  1949  		p.buf.WriteString("\n")
  1950  		p.buf.WriteString(strings.Repeat(" ", indent))
  1951  		p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun))
  1952  	}
  1953  }
  1954  
  1955  func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) {
  1956  	if skipped > 0 {
  1957  		noun := "elements"
  1958  		if skipped == 1 {
  1959  			noun = "element"
  1960  		}
  1961  		p.buf.WriteString(strings.Repeat(" ", indent))
  1962  		p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun))
  1963  		p.buf.WriteString("\n")
  1964  	}
  1965  }