package format import ( "bufio" "bytes" "fmt" "log" "sort" "strings" "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/states" ) // DiffLanguage controls the description of the resource change reasons. type DiffLanguage rune //go:generate go run golang.org/x/tools/cmd/stringer -type=DiffLanguage diff.go const ( // DiffLanguageProposedChange indicates that the change is one which is // planned to be applied. DiffLanguageProposedChange DiffLanguage = 'P' // DiffLanguageDetectedDrift indicates that the change is detected drift // from the configuration. DiffLanguageDetectedDrift DiffLanguage = 'D' ) // ResourceChange returns a string representation of a change to a particular // resource, for inclusion in user-facing plan output. // // The resource schema must be provided along with the change so that the // formatted change can reflect the configuration structure for the associated // resource. // // If "color" is non-nil, it will be used to color the result. Otherwise, // no color codes will be included. func ResourceChange( change *plans.ResourceInstanceChangeSrc, schema *configschema.Block, color *colorstring.Colorize, language DiffLanguage, ) string { addr := change.Addr var buf bytes.Buffer if color == nil { color = &colorstring.Colorize{ Colors: colorstring.DefaultColors, Disable: true, Reset: false, } } dispAddr := addr.String() if change.DeposedKey != states.NotDeposed { dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey) } switch change.Action { case plans.Create: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be created"), dispAddr)) case plans.Read: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be read during apply\n # (config refers to values not yet known)"), dispAddr)) case plans.Update: switch language { case DiffLanguageProposedChange: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be updated in-place"), dispAddr)) case DiffLanguageDetectedDrift: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has changed"), dispAddr)) default: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] update (unknown reason %s)"), dispAddr, language)) } case plans.CreateThenDelete, plans.DeleteThenCreate: switch change.ActionReason { case plans.ResourceInstanceReplaceBecauseTainted: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] is tainted, so must be [bold][red]replaced"), dispAddr)) case plans.ResourceInstanceReplaceByRequest: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested"), dispAddr)) default: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] must be [bold][red]replaced"), dispAddr)) } case plans.Delete: switch language { case DiffLanguageProposedChange: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]destroyed"), dispAddr)) case DiffLanguageDetectedDrift: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has been deleted"), dispAddr)) default: buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] delete (unknown reason %s)"), dispAddr, language)) } // We can sometimes give some additional detail about why we're // proposing to delete. We show this as additional notes, rather than // as additional wording in the main action statement, in an attempt // to make the "will be destroyed" message prominent and consistent // in all cases, for easier scanning of this often-risky action. switch change.ActionReason { case plans.ResourceInstanceDeleteBecauseNoResourceConfig: buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", addr.Resource.Resource)) case plans.ResourceInstanceDeleteBecauseNoModule: // FIXME: Ideally we'd truncate addr.Module to reflect the earliest // step that doesn't exist, so it's clearer which call this refers // to, but we don't have enough information out here in the UI layer // to decide that; only the "expander" in Terraform Core knows // which module instance keys are actually declared. buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", addr.Module)) case plans.ResourceInstanceDeleteBecauseWrongRepetition: // We have some different variations of this one switch addr.Resource.Key.(type) { case nil: buf.WriteString("\n # (because resource uses count or for_each)") case addrs.IntKey: buf.WriteString("\n # (because resource does not use count)") case addrs.StringKey: buf.WriteString("\n # (because resource does not use for_each)") } case plans.ResourceInstanceDeleteBecauseCountIndex: buf.WriteString(fmt.Sprintf("\n # (because index %s is out of range for count)", addr.Resource.Key)) case plans.ResourceInstanceDeleteBecauseEachKey: buf.WriteString(fmt.Sprintf("\n # (because key %s is not in for_each map)", addr.Resource.Key)) } if change.DeposedKey != states.NotDeposed { // Some extra context about this unusual situation. buf.WriteString(color.Color("\n # (left over from a partially-failed replacement of this instance)")) } case plans.NoOp: if change.Moved() { buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has moved to [bold]%s[reset]"), change.PrevRunAddr.String(), dispAddr)) break } fallthrough default: // should never happen, since the above is exhaustive buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) } buf.WriteString(color.Color("[reset]\n")) if change.Moved() && change.Action != plans.NoOp { buf.WriteString(fmt.Sprintf(color.Color(" # [reset](moved from %s)\n"), change.PrevRunAddr.String())) } if change.Moved() && change.Action == plans.NoOp { buf.WriteString(" ") } else { buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ") } switch addr.Resource.Resource.Mode { case addrs.ManagedResourceMode: buf.WriteString(fmt.Sprintf( "resource %q %q", addr.Resource.Resource.Type, addr.Resource.Resource.Name, )) case addrs.DataResourceMode: buf.WriteString(fmt.Sprintf( "data %q %q ", addr.Resource.Resource.Type, addr.Resource.Resource.Name, )) default: // should never happen, since the above is exhaustive buf.WriteString(addr.String()) } buf.WriteString(" {") p := blockBodyDiffPrinter{ buf: &buf, color: color, action: change.Action, requiredReplace: change.RequiredReplace, } // Most commonly-used resources have nested blocks that result in us // going at least three traversals deep while we recurse here, so we'll // start with that much capacity and then grow as needed for deeper // structures. path := make(cty.Path, 0, 3) changeV, err := change.Decode(schema.ImpliedType()) if err != nil { // Should never happen in here, since we've already been through // loads of layers of encode/decode of the planned changes before now. panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", addr, err)) } // We currently have an opt-out that permits the legacy SDK to return values // that defy our usual conventions around handling of nesting blocks. To // avoid the rendering code from needing to handle all of these, we'll // normalize first. // (Ideally we'd do this as part of the SDK opt-out implementation in core, // but we've added it here for now to reduce risk of unexpected impacts // on other code in core.) changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) result := p.writeBlockBodyDiff(schema, changeV.Before, changeV.After, 6, path) if result.bodyWritten { buf.WriteString("\n") buf.WriteString(strings.Repeat(" ", 4)) } buf.WriteString("}\n") return buf.String() } // OutputChanges returns a string representation of a set of changes to output // values for inclusion in user-facing plan output. // // If "color" is non-nil, it will be used to color the result. Otherwise, // no color codes will be included. func OutputChanges( changes []*plans.OutputChangeSrc, color *colorstring.Colorize, ) string { var buf bytes.Buffer p := blockBodyDiffPrinter{ buf: &buf, color: color, action: plans.Update, // not actually used in this case, because we're not printing a containing block } // We're going to reuse the codepath we used for printing resource block // diffs, by pretending that the set of defined outputs are the attributes // of some resource. It's a little forced to do this, but it gives us all // the same formatting heuristics as we normally use for resource // attributes. oldVals := make(map[string]cty.Value, len(changes)) newVals := make(map[string]cty.Value, len(changes)) synthSchema := &configschema.Block{ Attributes: make(map[string]*configschema.Attribute, len(changes)), } for _, changeSrc := range changes { name := changeSrc.Addr.OutputValue.Name change, err := changeSrc.Decode() if err != nil { // It'd be weird to get a decoding error here because that would // suggest that Terraform itself just produced an invalid plan, and // we don't have any good way to ignore it in this codepath, so // we'll just log it and ignore it. log.Printf("[ERROR] format.OutputChanges: Failed to decode planned change for output %q: %s", name, err) continue } synthSchema.Attributes[name] = &configschema.Attribute{ Type: cty.DynamicPseudoType, // output types are decided dynamically based on the given value Optional: true, Sensitive: change.Sensitive, } oldVals[name] = change.Before newVals[name] = change.After } p.writeBlockBodyDiff(synthSchema, cty.ObjectVal(oldVals), cty.ObjectVal(newVals), 2, nil) return buf.String() } type blockBodyDiffPrinter struct { buf *bytes.Buffer color *colorstring.Colorize action plans.Action requiredReplace cty.PathSet // verbose is set to true when using the "diff" printer to format state verbose bool } type blockBodyDiffResult struct { bodyWritten bool skippedAttributes int skippedBlocks int } const forcesNewResourceCaption = " [red]# forces replacement[reset]" // writeBlockBodyDiff writes attribute or block differences // and returns true if any differences were found and written func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult { path = ctyEnsurePathCapacity(path, 1) result := blockBodyDiffResult{} // write the attributes diff blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result) p.writeSkippedAttr(result.skippedAttributes, indent+2) { blockTypeNames := make([]string, 0, len(schema.BlockTypes)) for name := range schema.BlockTypes { blockTypeNames = append(blockTypeNames, name) } sort.Strings(blockTypeNames) for _, name := range blockTypeNames { blockS := schema.BlockTypes[name] oldVal := ctyGetAttrMaybeNull(old, name) newVal := ctyGetAttrMaybeNull(new, name) result.bodyWritten = true skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) if skippedBlocks > 0 { result.skippedBlocks += skippedBlocks } // Always include a blank for any subsequent block types. blankBeforeBlocks = true } if result.skippedBlocks > 0 { noun := "blocks" if result.skippedBlocks == 1 { noun = "block" } p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), result.skippedBlocks, noun)) } } return result } func (p *blockBodyDiffPrinter) writeAttrsDiff( attrsS map[string]*configschema.Attribute, old, new cty.Value, indent int, path cty.Path, result *blockBodyDiffResult) bool { blankBeforeBlocks := false attrNames := make([]string, 0, len(attrsS)) attrNameLen := 0 for name := range attrsS { oldVal := ctyGetAttrMaybeNull(old, name) newVal := ctyGetAttrMaybeNull(new, name) if oldVal.IsNull() && newVal.IsNull() { // Skip attributes where both old and new values are null // (we do this early here so that we'll do our value alignment // based on the longest attribute name that has a change, rather // than the longest attribute name in the full set.) continue } attrNames = append(attrNames, name) if len(name) > attrNameLen { attrNameLen = len(name) } } sort.Strings(attrNames) if len(attrNames) > 0 { blankBeforeBlocks = true } for _, name := range attrNames { attrS := attrsS[name] oldVal := ctyGetAttrMaybeNull(old, name) newVal := ctyGetAttrMaybeNull(new, name) result.bodyWritten = true skipped := p.writeAttrDiff(name, attrS, oldVal, newVal, attrNameLen, indent, path) if skipped { result.skippedAttributes++ } } return blankBeforeBlocks } // getPlanActionAndShow returns the action value // and a boolean for showJustNew. In this function we // modify the old and new values to remove any possible marks func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) { var action plans.Action showJustNew := false switch { case old.IsNull(): action = plans.Create showJustNew = true case new.IsNull(): action = plans.Delete case ctyEqualWithUnknown(old, new): action = plans.NoOp showJustNew = true default: action = plans.Update } return action, showJustNew } func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool { path = append(path, cty.GetAttrStep{Name: name}) action, showJustNew := getPlanActionAndShow(old, new) if action == plans.NoOp && !p.verbose && !identifyingAttribute(name, attrS) { return true } if attrS.NestedType != nil { p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew) return false } p.buf.WriteString("\n") p.writeSensitivityWarning(old, new, indent, action, false) p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) p.buf.WriteString(p.color.Color("[bold]")) p.buf.WriteString(name) p.buf.WriteString(p.color.Color("[reset]")) p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) p.buf.WriteString(" = ") if attrS.Sensitive { p.buf.WriteString("(sensitive value)") if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } } else { switch { case showJustNew: p.writeValue(new, action, indent+2) if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } default: // We show new even if it is null to emphasize the fact // that it is being unset, since otherwise it is easy to // misunderstand that the value is still set to the old value. p.writeValueDiff(old, new, indent+2, path) } } return false } // writeNestedAttrDiff is responsible for formatting Attributes with NestedTypes // in the diff. func (p *blockBodyDiffPrinter) writeNestedAttrDiff( name string, objS *configschema.Object, old, new cty.Value, nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) { p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) p.buf.WriteString(p.color.Color("[bold]")) p.buf.WriteString(name) p.buf.WriteString(p.color.Color("[reset]")) p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) result := &blockBodyDiffResult{} switch objS.Nesting { case configschema.NestingSingle: p.buf.WriteString(" = {") if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.writeAttrsDiff(objS.Attributes, old, new, indent+4, path, result) p.writeSkippedAttr(result.skippedAttributes, indent+6) p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("}") case configschema.NestingList: p.buf.WriteString(" = [") if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.buf.WriteString("\n") oldItems := ctyCollectionValues(old) newItems := ctyCollectionValues(new) // Here we intentionally preserve the index-based correspondance // between old and new, rather than trying to detect insertions // and removals in the list, because this more accurately reflects // how Terraform Core and providers will understand the change, // particularly when the nested block contains computed attributes // that will themselves maintain correspondance by index. // commonLen is number of elements that exist in both lists, which // will be presented as updates (~). Any additional items in one // of the lists will be presented as either creates (+) or deletes (-) // depending on which list they belong to. maxLen is the number of // elements in that longer list. var commonLen int var maxLen int // unchanged is the number of unchanged elements var unchanged int switch { case len(oldItems) < len(newItems): commonLen = len(oldItems) maxLen = len(newItems) default: commonLen = len(newItems) maxLen = len(oldItems) } for i := 0; i < maxLen; i++ { path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) var action plans.Action var oldItem, newItem cty.Value switch { case i < commonLen: oldItem = oldItems[i] newItem = newItems[i] if oldItem.RawEquals(newItem) { action = plans.NoOp unchanged++ } else { action = plans.Update } case i < len(oldItems): oldItem = oldItems[i] newItem = cty.NullVal(oldItem.Type()) action = plans.Delete case i < len(newItems): newItem = newItems[i] oldItem = cty.NullVal(newItem.Type()) action = plans.Create default: action = plans.NoOp } if action != plans.NoOp { p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeActionSymbol(action) p.buf.WriteString("{") result := &blockBodyDiffResult{} p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+8, path, result) if action == plans.Update { p.writeSkippedAttr(result.skippedAttributes, indent+10) } p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+6)) p.buf.WriteString("},\n") } } p.writeSkippedElems(unchanged, indent+6) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("]") if !new.IsKnown() { p.buf.WriteString(" -> (known after apply)") } case configschema.NestingSet: oldItems := ctyCollectionValues(old) newItems := ctyCollectionValues(new) var all cty.Value if len(oldItems)+len(newItems) > 0 { allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) allItems = append(allItems, oldItems...) allItems = append(allItems, newItems...) all = cty.SetVal(allItems) } else { all = cty.SetValEmpty(old.Type().ElementType()) } p.buf.WriteString(" = [") var unchanged int for it := all.ElementIterator(); it.Next(); { _, val := it.Element() var action plans.Action var oldValue, newValue cty.Value switch { case !val.IsKnown(): action = plans.Update newValue = val case !new.IsKnown(): action = plans.Delete // the value must have come from the old set oldValue = val // Mark the new val as null, but the entire set will be // displayed as "(unknown after apply)" newValue = cty.NullVal(val.Type()) case old.IsNull() || !old.HasElement(val).True(): action = plans.Create oldValue = cty.NullVal(val.Type()) newValue = val case new.IsNull() || !new.HasElement(val).True(): action = plans.Delete oldValue = val newValue = cty.NullVal(val.Type()) default: action = plans.NoOp oldValue = val newValue = val } if action == plans.NoOp { unchanged++ continue } p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeActionSymbol(action) p.buf.WriteString("{") if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } path := append(path, cty.IndexStep{Key: val}) p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+8, path, result) p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+6)) p.buf.WriteString("},") } p.buf.WriteString("\n") p.writeSkippedElems(unchanged, indent+6) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("]") if !new.IsKnown() { p.buf.WriteString(" -> (known after apply)") } case configschema.NestingMap: // For the sake of handling nested blocks, we'll treat a null map // the same as an empty map since the config language doesn't // distinguish these anyway. old = ctyNullBlockMapAsEmpty(old) new = ctyNullBlockMapAsEmpty(new) oldItems := old.AsValueMap() newItems := map[string]cty.Value{} if new.IsKnown() { newItems = new.AsValueMap() } allKeys := make(map[string]bool) for k := range oldItems { allKeys[k] = true } for k := range newItems { allKeys[k] = true } allKeysOrder := make([]string, 0, len(allKeys)) for k := range allKeys { allKeysOrder = append(allKeysOrder, k) } sort.Strings(allKeysOrder) p.buf.WriteString(" = {\n") // unchanged tracks the number of unchanged elements unchanged := 0 for _, k := range allKeysOrder { var action plans.Action oldValue := oldItems[k] newValue := newItems[k] switch { case oldValue == cty.NilVal: oldValue = cty.NullVal(newValue.Type()) action = plans.Create case newValue == cty.NilVal: newValue = cty.NullVal(oldValue.Type()) action = plans.Delete case !newValue.RawEquals(oldValue): action = plans.Update default: action = plans.NoOp unchanged++ } if action != plans.NoOp { p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeActionSymbol(action) fmt.Fprintf(p.buf, "%q = {", k) if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+8, path, result) p.writeSkippedAttr(result.skippedAttributes, indent+10) p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+6)) p.buf.WriteString("},\n") } } p.writeSkippedElems(unchanged, indent+6) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("}") if !new.IsKnown() { p.buf.WriteString(" -> (known after apply)") } } } func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int { skippedBlocks := 0 path = append(path, cty.GetAttrStep{Name: name}) if old.IsNull() && new.IsNull() { // Nothing to do if both old and new is null return skippedBlocks } // If either the old or the new value is marked, // Display a special diff because it is irrelevant // to list all obfuscated attributes as (sensitive) if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) { p.writeSensitiveNestedBlockDiff(name, old, new, indent, blankBefore, path) return 0 } // Where old/new are collections representing a nesting mode other than // NestingSingle, we assume the collection value can never be unknown // since we always produce the container for the nested objects, even if // the objects within are computed. switch blockS.Nesting { case configschema.NestingSingle, configschema.NestingGroup: var action plans.Action eqV := new.Equals(old) switch { case old.IsNull(): action = plans.Create case new.IsNull(): action = plans.Delete case !new.IsWhollyKnown() || !old.IsWhollyKnown(): // "old" should actually always be known due to our contract // that old values must never be unknown, but we'll allow it // anyway to be robust. action = plans.Update case !eqV.IsKnown() || !eqV.True(): action = plans.Update } if blankBefore { p.buf.WriteRune('\n') } skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, path) if skipped { return 1 } case configschema.NestingList: // For the sake of handling nested blocks, we'll treat a null list // the same as an empty list since the config language doesn't // distinguish these anyway. old = ctyNullBlockListAsEmpty(old) new = ctyNullBlockListAsEmpty(new) oldItems := ctyCollectionValues(old) newItems := ctyCollectionValues(new) // Here we intentionally preserve the index-based correspondance // between old and new, rather than trying to detect insertions // and removals in the list, because this more accurately reflects // how Terraform Core and providers will understand the change, // particularly when the nested block contains computed attributes // that will themselves maintain correspondance by index. // commonLen is number of elements that exist in both lists, which // will be presented as updates (~). Any additional items in one // of the lists will be presented as either creates (+) or deletes (-) // depending on which list they belong to. var commonLen int switch { case len(oldItems) < len(newItems): commonLen = len(oldItems) default: commonLen = len(newItems) } if blankBefore && (len(oldItems) > 0 || len(newItems) > 0) { p.buf.WriteRune('\n') } for i := 0; i < commonLen; i++ { path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) oldItem := oldItems[i] newItem := newItems[i] action := plans.Update if oldItem.RawEquals(newItem) { action = plans.NoOp } skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, path) if skipped { skippedBlocks++ } } for i := commonLen; i < len(oldItems); i++ { path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) oldItem := oldItems[i] newItem := cty.NullVal(oldItem.Type()) skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, path) if skipped { skippedBlocks++ } } for i := commonLen; i < len(newItems); i++ { path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) newItem := newItems[i] oldItem := cty.NullVal(newItem.Type()) skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, path) if skipped { skippedBlocks++ } } case configschema.NestingSet: // For the sake of handling nested blocks, we'll treat a null set // the same as an empty set since the config language doesn't // distinguish these anyway. old = ctyNullBlockSetAsEmpty(old) new = ctyNullBlockSetAsEmpty(new) oldItems := ctyCollectionValues(old) newItems := ctyCollectionValues(new) if (len(oldItems) + len(newItems)) == 0 { // Nothing to do if both sets are empty return 0 } allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) allItems = append(allItems, oldItems...) allItems = append(allItems, newItems...) all := cty.SetVal(allItems) if blankBefore { p.buf.WriteRune('\n') } for it := all.ElementIterator(); it.Next(); { _, val := it.Element() var action plans.Action var oldValue, newValue cty.Value switch { case !val.IsKnown(): action = plans.Update newValue = val case !old.HasElement(val).True(): action = plans.Create oldValue = cty.NullVal(val.Type()) newValue = val case !new.HasElement(val).True(): action = plans.Delete oldValue = val newValue = cty.NullVal(val.Type()) default: action = plans.NoOp oldValue = val newValue = val } path := append(path, cty.IndexStep{Key: val}) skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, path) if skipped { skippedBlocks++ } } case configschema.NestingMap: // For the sake of handling nested blocks, we'll treat a null map // the same as an empty map since the config language doesn't // distinguish these anyway. old = ctyNullBlockMapAsEmpty(old) new = ctyNullBlockMapAsEmpty(new) oldItems := old.AsValueMap() newItems := new.AsValueMap() if (len(oldItems) + len(newItems)) == 0 { // Nothing to do if both maps are empty return 0 } allKeys := make(map[string]bool) for k := range oldItems { allKeys[k] = true } for k := range newItems { allKeys[k] = true } allKeysOrder := make([]string, 0, len(allKeys)) for k := range allKeys { allKeysOrder = append(allKeysOrder, k) } sort.Strings(allKeysOrder) if blankBefore { p.buf.WriteRune('\n') } for _, k := range allKeysOrder { var action plans.Action oldValue := oldItems[k] newValue := newItems[k] switch { case oldValue == cty.NilVal: oldValue = cty.NullVal(newValue.Type()) action = plans.Create case newValue == cty.NilVal: newValue = cty.NullVal(oldValue.Type()) action = plans.Delete case !newValue.RawEquals(oldValue): action = plans.Update default: action = plans.NoOp } path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, path) if skipped { skippedBlocks++ } } } return skippedBlocks } func (p *blockBodyDiffPrinter) writeSensitiveNestedBlockDiff(name string, old, new cty.Value, indent int, blankBefore bool, path cty.Path) { var action plans.Action switch { case old.IsNull(): action = plans.Create case new.IsNull(): action = plans.Delete case !new.IsWhollyKnown() || !old.IsWhollyKnown(): // "old" should actually always be known due to our contract // that old values must never be unknown, but we'll allow it // anyway to be robust. action = plans.Update case !ctyEqualValueAndMarks(old, new): action = plans.Update } if blankBefore { p.buf.WriteRune('\n') } // New line before warning printing p.buf.WriteRune('\n') p.writeSensitivityWarning(old, new, indent, action, true) p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) fmt.Fprintf(p.buf, "%s {", name) if action != plans.NoOp && p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.buf.WriteRune('\n') p.buf.WriteString(strings.Repeat(" ", indent+4)) p.buf.WriteString("# At least one attribute in this block is (or was) sensitive,\n") p.buf.WriteString(strings.Repeat(" ", indent+4)) p.buf.WriteString("# so its contents will not be displayed.") p.buf.WriteRune('\n') p.buf.WriteString(strings.Repeat(" ", indent+2)) p.buf.WriteString("}") } func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, path cty.Path) bool { if action == plans.NoOp && !p.verbose { return true } p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) p.writeActionSymbol(action) if label != nil { fmt.Fprintf(p.buf, "%s %q {", name, *label) } else { fmt.Fprintf(p.buf, "%s {", name) } if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) if result.bodyWritten { p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) } p.buf.WriteString("}") return false } func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { // Could check specifically for the sensitivity marker if val.HasMark(marks.Sensitive) { p.buf.WriteString("(sensitive)") return } if !val.IsKnown() { p.buf.WriteString("(known after apply)") return } if val.IsNull() { p.buf.WriteString(p.color.Color("[dark_gray]null[reset]")) return } ty := val.Type() switch { case ty.IsPrimitiveType(): switch ty { case cty.String: { // Special behavior for JSON strings containing array or object src := []byte(val.AsString()) ty, err := ctyjson.ImpliedType(src) // check for the special case of "null", which decodes to nil, // and just allow it to be printed out directly if err == nil && !ty.IsPrimitiveType() && strings.TrimSpace(val.AsString()) != "null" { jv, err := ctyjson.Unmarshal(src, ty) if err == nil { p.buf.WriteString("jsonencode(") if jv.LengthInt() == 0 { p.writeValue(jv, action, 0) } else { p.buf.WriteByte('\n') p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeValue(jv, action, indent+4) p.buf.WriteByte('\n') p.buf.WriteString(strings.Repeat(" ", indent)) } p.buf.WriteByte(')') break // don't *also* do the normal behavior below } } } if strings.Contains(val.AsString(), "\n") { // It's a multi-line string, so we want to use the multi-line // rendering so it'll be readable. Rather than re-implement // that here, we'll just re-use the multi-line string diff // printer with no changes, which ends up producing the // result we want here. // The path argument is nil because we don't track path // information into strings and we know that a string can't // have any indices or attributes that might need to be marked // as (requires replacement), which is what that argument is for. p.writeValueDiff(val, val, indent, nil) break } fmt.Fprintf(p.buf, "%q", val.AsString()) case cty.Bool: if val.True() { p.buf.WriteString("true") } else { p.buf.WriteString("false") } case cty.Number: bf := val.AsBigFloat() p.buf.WriteString(bf.Text('f', -1)) default: // should never happen, since the above is exhaustive fmt.Fprintf(p.buf, "%#v", val) } case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): p.buf.WriteString("[") it := val.ElementIterator() for it.Next() { _, val := it.Element() p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.writeValue(val, action, indent+4) p.buf.WriteString(",") } if val.LengthInt() > 0 { p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) } p.buf.WriteString("]") case ty.IsMapType(): p.buf.WriteString("{") keyLen := 0 for it := val.ElementIterator(); it.Next(); { key, _ := it.Element() if keyStr := key.AsString(); len(keyStr) > keyLen { keyLen = len(keyStr) } } for it := val.ElementIterator(); it.Next(); { key, val := it.Element() p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.writeValue(key, action, indent+4) p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString()))) p.buf.WriteString(" = ") p.writeValue(val, action, indent+4) } if val.LengthInt() > 0 { p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) } p.buf.WriteString("}") case ty.IsObjectType(): p.buf.WriteString("{") atys := ty.AttributeTypes() attrNames := make([]string, 0, len(atys)) nameLen := 0 for attrName := range atys { attrNames = append(attrNames, attrName) if len(attrName) > nameLen { nameLen = len(attrName) } } sort.Strings(attrNames) for _, attrName := range attrNames { val := val.GetAttr(attrName) p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.buf.WriteString(attrName) p.buf.WriteString(strings.Repeat(" ", nameLen-len(attrName))) p.buf.WriteString(" = ") p.writeValue(val, action, indent+4) } if len(attrNames) > 0 { p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) } p.buf.WriteString("}") } } func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) { ty := old.Type() typesEqual := ctyTypesEqual(ty, new.Type()) // We have some specialized diff implementations for certain complex // values where it's useful to see a visualization of the diff of // the nested elements rather than just showing the entire old and // new values verbatim. // However, these specialized implementations can apply only if both // values are known and non-null. if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual { if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) { p.buf.WriteString("(sensitive)") if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } return } switch { case ty == cty.String: // We have special behavior for both multi-line strings in general // and for strings that can parse as JSON. For the JSON handling // to apply, both old and new must be valid JSON. // For single-line strings that don't parse as JSON we just fall // out of this switch block and do the default old -> new rendering. oldS := old.AsString() newS := new.AsString() { // Special behavior for JSON strings containing object or // list values. oldBytes := []byte(oldS) newBytes := []byte(newS) oldType, oldErr := ctyjson.ImpliedType(oldBytes) newType, newErr := ctyjson.ImpliedType(newBytes) if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) { oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType) newJV, newErr := ctyjson.Unmarshal(newBytes, newType) if oldErr == nil && newErr == nil { if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace p.buf.WriteString("jsonencode(") p.buf.WriteByte('\n') p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(plans.Update) p.writeValueDiff(oldJV, newJV, indent+4, path) p.buf.WriteByte('\n') p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteByte(')') } else { // if they differ only in insignificant whitespace // then we'll note that but still expand out the // effective value. if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]")) } else { p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]")) } p.buf.WriteByte('\n') p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeValue(oldJV, plans.NoOp, indent+4) p.buf.WriteByte('\n') p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteByte(')') } return } } } if !strings.Contains(oldS, "\n") && !strings.Contains(newS, "\n") { break } p.buf.WriteString("<<-EOT") if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.buf.WriteString("\n") var oldLines, newLines []cty.Value { r := strings.NewReader(oldS) sc := bufio.NewScanner(r) for sc.Scan() { oldLines = append(oldLines, cty.StringVal(sc.Text())) } } { r := strings.NewReader(newS) sc := bufio.NewScanner(r) for sc.Scan() { newLines = append(newLines, cty.StringVal(sc.Text())) } } // Optimization for strings which are exactly equal: just print // directly without calculating the sequence diff. This makes a // significant difference when this code path is reached via a // writeValue call with a large multi-line string. if oldS == newS { for _, line := range newLines { p.buf.WriteString(strings.Repeat(" ", indent+4)) p.buf.WriteString(line.AsString()) p.buf.WriteString("\n") } } else { diffLines := ctySequenceDiff(oldLines, newLines) for _, diffLine := range diffLines { p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(diffLine.Action) switch diffLine.Action { case plans.NoOp, plans.Delete: p.buf.WriteString(diffLine.Before.AsString()) case plans.Create: p.buf.WriteString(diffLine.After.AsString()) default: // Should never happen since the above covers all // actions that ctySequenceDiff can return for strings p.buf.WriteString(diffLine.After.AsString()) } p.buf.WriteString("\n") } } p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol p.buf.WriteString("EOT") return case ty.IsSetType(): p.buf.WriteString("[") if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.buf.WriteString("\n") var addedVals, removedVals, allVals []cty.Value for it := old.ElementIterator(); it.Next(); { _, val := it.Element() allVals = append(allVals, val) if new.HasElement(val).False() { removedVals = append(removedVals, val) } } for it := new.ElementIterator(); it.Next(); { _, val := it.Element() allVals = append(allVals, val) if val.IsKnown() && old.HasElement(val).False() { addedVals = append(addedVals, val) } } var all, added, removed cty.Value if len(allVals) > 0 { all = cty.SetVal(allVals) } else { all = cty.SetValEmpty(ty.ElementType()) } if len(addedVals) > 0 { added = cty.SetVal(addedVals) } else { added = cty.SetValEmpty(ty.ElementType()) } if len(removedVals) > 0 { removed = cty.SetVal(removedVals) } else { removed = cty.SetValEmpty(ty.ElementType()) } suppressedElements := 0 for it := all.ElementIterator(); it.Next(); { _, val := it.Element() var action plans.Action switch { case !val.IsKnown(): action = plans.Update case added.HasElement(val).True(): action = plans.Create case removed.HasElement(val).True(): action = plans.Delete default: action = plans.NoOp } if action == plans.NoOp && !p.verbose { suppressedElements++ continue } p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.writeValue(val, action, indent+4) p.buf.WriteString(",\n") } if suppressedElements > 0 { p.writeActionSymbol(plans.NoOp) p.buf.WriteString(strings.Repeat(" ", indent+2)) noun := "elements" if suppressedElements == 1 { noun = "element" } p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun)) p.buf.WriteString("\n") } p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("]") return case ty.IsListType() || ty.IsTupleType(): p.buf.WriteString("[") if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.buf.WriteString("\n") elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice()) // Maintain a stack of suppressed lines in the diff for later // display or elision var suppressedElements []*plans.Change var changeShown bool for i := 0; i < len(elemDiffs); i++ { if !p.verbose { for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp { suppressedElements = append(suppressedElements, elemDiffs[i]) i++ } } // If we have some suppressed elements on the stackā€¦ if len(suppressedElements) > 0 { // If we've just rendered a change, display the first // element in the stack as context if changeShown { elemDiff := suppressedElements[0] p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeValue(elemDiff.After, elemDiff.Action, indent+4) p.buf.WriteString(",\n") suppressedElements = suppressedElements[1:] } hidden := len(suppressedElements) // If we're not yet at the end of the list, capture the // last element on the stack as context for the upcoming // change to be rendered var nextContextDiff *plans.Change if hidden > 0 && i < len(elemDiffs) { hidden-- nextContextDiff = suppressedElements[hidden] } // If there are still hidden elements, show an elision // statement counting them if hidden > 0 { p.writeActionSymbol(plans.NoOp) p.buf.WriteString(strings.Repeat(" ", indent+2)) noun := "elements" if hidden == 1 { noun = "element" } p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), hidden, noun)) p.buf.WriteString("\n") } // Display the next context diff if it was captured above if nextContextDiff != nil { p.buf.WriteString(strings.Repeat(" ", indent+4)) p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4) p.buf.WriteString(",\n") } // Suppressed elements have now been handled so clear them again suppressedElements = nil } if i >= len(elemDiffs) { break } elemDiff := elemDiffs[i] p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(elemDiff.Action) switch elemDiff.Action { case plans.NoOp, plans.Delete: p.writeValue(elemDiff.Before, elemDiff.Action, indent+4) case plans.Update: p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path) case plans.Create: p.writeValue(elemDiff.After, elemDiff.Action, indent+4) default: // Should never happen since the above covers all // actions that ctySequenceDiff can return. p.writeValue(elemDiff.After, elemDiff.Action, indent+4) } p.buf.WriteString(",\n") changeShown = true } p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("]") return case ty.IsMapType(): p.buf.WriteString("{") if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } p.buf.WriteString("\n") var allKeys []string keyLen := 0 for it := old.ElementIterator(); it.Next(); { k, _ := it.Element() keyStr := k.AsString() allKeys = append(allKeys, keyStr) if len(keyStr) > keyLen { keyLen = len(keyStr) } } for it := new.ElementIterator(); it.Next(); { k, _ := it.Element() keyStr := k.AsString() allKeys = append(allKeys, keyStr) if len(keyStr) > keyLen { keyLen = len(keyStr) } } sort.Strings(allKeys) suppressedElements := 0 lastK := "" for i, k := range allKeys { if i > 0 && lastK == k { continue // skip duplicates (list is sorted) } lastK = k kV := cty.StringVal(k) var action plans.Action if old.HasIndex(kV).False() { action = plans.Create } else if new.HasIndex(kV).False() { action = plans.Delete } if old.HasIndex(kV).True() && new.HasIndex(kV).True() { if ctyEqualValueAndMarks(old.Index(kV), new.Index(kV)) { action = plans.NoOp } else { action = plans.Update } } if action == plans.NoOp && !p.verbose { suppressedElements++ continue } path := append(path, cty.IndexStep{Key: kV}) oldV := old.Index(kV) newV := new.Index(kV) p.writeSensitivityWarning(oldV, newV, indent+2, action, false) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.writeValue(kV, action, indent+4) p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) p.buf.WriteString(" = ") switch action { case plans.Create, plans.NoOp: v := new.Index(kV) if v.HasMark(marks.Sensitive) { p.buf.WriteString("(sensitive)") } else { p.writeValue(v, action, indent+4) } case plans.Delete: oldV := old.Index(kV) newV := cty.NullVal(oldV.Type()) p.writeValueDiff(oldV, newV, indent+4, path) default: if oldV.HasMark(marks.Sensitive) || newV.HasMark(marks.Sensitive) { p.buf.WriteString("(sensitive)") } else { p.writeValueDiff(oldV, newV, indent+4, path) } } p.buf.WriteByte('\n') } if suppressedElements > 0 { p.writeActionSymbol(plans.NoOp) p.buf.WriteString(strings.Repeat(" ", indent+2)) noun := "elements" if suppressedElements == 1 { noun = "element" } p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun)) p.buf.WriteString("\n") } p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("}") return case ty.IsObjectType(): p.buf.WriteString("{") p.buf.WriteString("\n") forcesNewResource := p.pathForcesNewResource(path) var allKeys []string keyLen := 0 for it := old.ElementIterator(); it.Next(); { k, _ := it.Element() keyStr := k.AsString() allKeys = append(allKeys, keyStr) if len(keyStr) > keyLen { keyLen = len(keyStr) } } for it := new.ElementIterator(); it.Next(); { k, _ := it.Element() keyStr := k.AsString() allKeys = append(allKeys, keyStr) if len(keyStr) > keyLen { keyLen = len(keyStr) } } sort.Strings(allKeys) suppressedElements := 0 lastK := "" for i, k := range allKeys { if i > 0 && lastK == k { continue // skip duplicates (list is sorted) } lastK = k kV := k var action plans.Action if !old.Type().HasAttribute(kV) { action = plans.Create } else if !new.Type().HasAttribute(kV) { action = plans.Delete } else if ctyEqualValueAndMarks(old.GetAttr(kV), new.GetAttr(kV)) { action = plans.NoOp } else { action = plans.Update } if action == plans.NoOp && !p.verbose { suppressedElements++ continue } path := append(path, cty.GetAttrStep{Name: kV}) p.buf.WriteString(strings.Repeat(" ", indent+2)) p.writeActionSymbol(action) p.buf.WriteString(k) p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) p.buf.WriteString(" = ") switch action { case plans.Create, plans.NoOp: v := new.GetAttr(kV) p.writeValue(v, action, indent+4) case plans.Delete: oldV := old.GetAttr(kV) newV := cty.NullVal(oldV.Type()) p.writeValueDiff(oldV, newV, indent+4, path) default: oldV := old.GetAttr(kV) newV := new.GetAttr(kV) p.writeValueDiff(oldV, newV, indent+4, path) } p.buf.WriteString("\n") } if suppressedElements > 0 { p.writeActionSymbol(plans.NoOp) p.buf.WriteString(strings.Repeat(" ", indent+2)) noun := "elements" if suppressedElements == 1 { noun = "element" } p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun)) p.buf.WriteString("\n") } p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString("}") if forcesNewResource { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } return } } // In all other cases, we just show the new and old values as-is p.writeValue(old, plans.Delete, indent) if new.IsNull() { p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] ")) } else { p.buf.WriteString(p.color.Color(" [yellow]->[reset] ")) } p.writeValue(new, plans.Create, indent) if p.pathForcesNewResource(path) { p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) } } // writeActionSymbol writes a symbol to represent the given action, followed // by a space. // // It only supports the actions that can be represented with a single character: // Create, Delete, Update and NoAction. func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) { switch action { case plans.Create: p.buf.WriteString(p.color.Color("[green]+[reset] ")) case plans.Delete: p.buf.WriteString(p.color.Color("[red]-[reset] ")) case plans.Update: p.buf.WriteString(p.color.Color("[yellow]~[reset] ")) case plans.NoOp: p.buf.WriteString(" ") default: // Should never happen p.buf.WriteString(p.color.Color("? ")) } } func (p *blockBodyDiffPrinter) writeSensitivityWarning(old, new cty.Value, indent int, action plans.Action, isBlock bool) { // Dont' show this warning for create or delete if action == plans.Create || action == plans.Delete { return } // Customize the warning based on if it is an attribute or block diffType := "attribute value" if isBlock { diffType = "block" } // If only attribute sensitivity is changing, clarify that the value is unchanged var valueUnchangedSuffix string if !isBlock { oldUnmarked, _ := old.UnmarkDeep() newUnmarked, _ := new.UnmarkDeep() if oldUnmarked.RawEquals(newUnmarked) { valueUnchangedSuffix = " The value is unchanged." } } if new.HasMark(marks.Sensitive) && !old.HasMark(marks.Sensitive) { p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will be marked as sensitive and will not\n"), diffType)) p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString(fmt.Sprintf("# display in UI output after applying this change.%s\n", valueUnchangedSuffix)) } // Note if changing this attribute will change its sensitivity if old.HasMark(marks.Sensitive) && !new.HasMark(marks.Sensitive) { p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will no longer be marked as sensitive\n"), diffType)) p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString(fmt.Sprintf("# after applying this change.%s\n", valueUnchangedSuffix)) } } func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool { if !p.action.IsReplace() || p.requiredReplace.Empty() { // "requiredReplace" only applies when the instance is being replaced, // and we should only inspect that set if it is not empty return false } return p.requiredReplace.Has(path) } func ctyEmptyString(value cty.Value) bool { if !value.IsNull() && value.IsKnown() { valueType := value.Type() if valueType == cty.String && value.AsString() == "" { return true } } return false } func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value { attrType := val.Type().AttributeType(name) if val.IsNull() { return cty.NullVal(attrType) } // We treat "" as null here // as existing SDK doesn't support null yet. // This allows us to avoid spurious diffs // until we introduce null to the SDK. attrValue := val.GetAttr(name) // If the value is marked, the ctyEmptyString function will fail if !val.ContainsMarked() && ctyEmptyString(attrValue) { return cty.NullVal(attrType) } return attrValue } func ctyCollectionValues(val cty.Value) []cty.Value { if !val.IsKnown() || val.IsNull() { return nil } ret := make([]cty.Value, 0, val.LengthInt()) for it := val.ElementIterator(); it.Next(); { _, value := it.Element() ret = append(ret, value) } return ret } // ctySequenceDiff returns differences between given sequences of cty.Value(s) // in the form of Create, Delete, or Update actions (for objects). func ctySequenceDiff(old, new []cty.Value) []*plans.Change { var ret []*plans.Change lcs := objchange.LongestCommonSubsequence(old, new) var oldI, newI, lcsI int for oldI < len(old) || newI < len(new) || lcsI < len(lcs) { // We first process items in the old and new sequences which are not // equal to the current common sequence item. Old items are marked as // deletions, and new items are marked as additions. // // There is an exception for deleted & created object items, which we // try to render as updates where that makes sense. for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) { // Render this as an object update if all of these are true: // // - the current old item is an object; // - there's a current new item which is also an object; // - either there are no common items left, or the current new item // doesn't equal the current common item. // // Why do we need the the last clause? If we have current items in all // three sequences, and the current new item is equal to a common item, // then we should just need to advance the old item list and we'll // eventually find a common item matching both old and new. // // This combination of conditions allows us to render an object update // diff instead of a combination of delete old & create new. isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) if isObjectDiff { ret = append(ret, &plans.Change{ Action: plans.Update, Before: old[oldI], After: new[newI], }) oldI++ newI++ // we also consume the next "new" in this case continue } // Otherwise, this item is not part of the common sequence, so // render as a deletion. ret = append(ret, &plans.Change{ Action: plans.Delete, Before: old[oldI], After: cty.NullVal(old[oldI].Type()), }) oldI++ } for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) { ret = append(ret, &plans.Change{ Action: plans.Create, Before: cty.NullVal(new[newI].Type()), After: new[newI], }) newI++ } // When we've exhausted the old & new sequences of items which are not // in the common subsequence, we render a common item and continue. if lcsI < len(lcs) { ret = append(ret, &plans.Change{ Action: plans.NoOp, Before: lcs[lcsI], After: lcs[lcsI], }) // All of our indexes advance together now, since the line // is common to all three sequences. lcsI++ oldI++ newI++ } } return ret } // ctyEqualValueAndMarks checks equality of two possibly-marked values, // considering partially-unknown values and equal values with different marks // as inequal func ctyEqualWithUnknown(old, new cty.Value) bool { if !old.IsWhollyKnown() || !new.IsWhollyKnown() { return false } return ctyEqualValueAndMarks(old, new) } // ctyEqualValueAndMarks checks equality of two possibly-marked values, // considering equal values with different marks as inequal func ctyEqualValueAndMarks(old, new cty.Value) bool { oldUnmarked, oldMarks := old.UnmarkDeep() newUnmarked, newMarks := new.UnmarkDeep() sameValue := oldUnmarked.Equals(newUnmarked) return sameValue.IsKnown() && sameValue.True() && oldMarks.Equal(newMarks) } // ctyTypesEqual checks equality of two types more loosely // by avoiding checks of object/tuple elements // as we render differences on element-by-element basis anyway func ctyTypesEqual(oldT, newT cty.Type) bool { if oldT.IsObjectType() && newT.IsObjectType() { return true } if oldT.IsTupleType() && newT.IsTupleType() { return true } return oldT.Equals(newT) } func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path { if cap(path)-len(path) >= minExtra { return path } newCap := cap(path) * 2 if newCap < (len(path) + minExtra) { newCap = len(path) + minExtra } newPath := make(cty.Path, len(path), newCap) copy(newPath, path) return newPath } // ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil // or returns an empty value of a suitable type to serve as a placeholder for it. // // In particular, this function handles the special situation where a "list" is // actually represented as a tuple type where nested blocks contain // dynamically-typed values. func ctyNullBlockListAsEmpty(in cty.Value) cty.Value { if !in.IsNull() { return in } if ty := in.Type(); ty.IsListType() { return cty.ListValEmpty(ty.ElementType()) } return cty.EmptyTupleVal // must need a tuple, then } // ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil // or returns an empty value of a suitable type to serve as a placeholder for it. // // In particular, this function handles the special situation where a "map" is // actually represented as an object type where nested blocks contain // dynamically-typed values. func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value { if !in.IsNull() { return in } if ty := in.Type(); ty.IsMapType() { return cty.MapValEmpty(ty.ElementType()) } return cty.EmptyObjectVal // must need an object, then } // ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil // or returns an empty value of a suitable type to serve as a placeholder for it. func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value { if !in.IsNull() { return in } // Dynamically-typed attributes are not supported inside blocks backed by // sets, so our result here is always a set. return cty.SetValEmpty(in.Type().ElementType()) } // DiffActionSymbol returns a string that, once passed through a // colorstring.Colorize, will produce a result that can be written // to a terminal to produce a symbol made of three printable // characters, possibly interspersed with VT100 color codes. func DiffActionSymbol(action plans.Action) string { switch action { case plans.DeleteThenCreate: return "[red]-[reset]/[green]+[reset]" case plans.CreateThenDelete: return "[green]+[reset]/[red]-[reset]" case plans.Create: return " [green]+[reset]" case plans.Delete: return " [red]-[reset]" case plans.Read: return " [cyan]<=[reset]" case plans.Update: return " [yellow]~[reset]" default: return " ?" } } // Extremely coarse heuristic for determining whether or not a given attribute // name is important for identifying a resource. In the future, this may be // replaced by a flag in the schema, but for now this is likely to be good // enough. func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool { return name == "id" || name == "tags" || name == "name" } func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) { if skipped > 0 { noun := "attributes" if skipped == 1 { noun = "attribute" } p.buf.WriteString("\n") p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun)) } } func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) { if skipped > 0 { noun := "elements" if skipped == 1 { noun = "element" } p.buf.WriteString(strings.Repeat(" ", indent)) p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun)) p.buf.WriteString("\n") } }