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