diff --git a/internal/command/format/diff.go b/internal/command/format/diff.go deleted file mode 100644 index fe8ec31e7e..0000000000 --- a/internal/command/format/diff.go +++ /dev/null @@ -1,2063 +0,0 @@ -package format - -import ( - "bufio" - "bytes" - "fmt" - "log" - "sort" - "strings" - - "github.com/hashicorp/hcl/v2/hclsyntax" - "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.ResourceInstanceChange, - 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"), dispAddr)) - switch change.ActionReason { - case plans.ResourceInstanceReadBecauseConfigUnknown: - buf.WriteString("\n # (config refers to values not yet known)") - case plans.ResourceInstanceReadBecauseDependencyPending: - buf.WriteString("\n # (depends on a resource or a module with changes pending)") - case plans.ResourceInstanceReadBecauseCheckNested: - buf.WriteString("\n # (data will be read during apply for a check block)") - } - 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)) - case plans.ResourceInstanceReplaceByTriggers: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by"), 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.ResourceInstanceDeleteBecauseNoMoveTarget: - buf.WriteString(fmt.Sprintf("\n # (because %s was moved to %s, which is not in configuration)", change.PrevRunAddr, 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) - - result := p.writeBlockBodyDiff(schema, change.Before, change.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]" - sensitiveCaption = "(sensitive value)" -) - -// 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\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 { - - attrNames := make([]string, 0, len(attrsS)) - displayAttrNames := make(map[string]string, 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) - displayAttrNames[name] = displayAttributeName(name) - if len(displayAttrNames[name]) > attrNameLen { - attrNameLen = len(displayAttrNames[name]) - } - } - sort.Strings(attrNames) - if len(attrNames) == 0 { - return false - } - - for _, name := range attrNames { - attrS := attrsS[name] - oldVal := ctyGetAttrMaybeNull(old, name) - newVal := ctyGetAttrMaybeNull(new, name) - - result.bodyWritten = true - skipped := p.writeAttrDiff(displayAttrNames[name], attrS, oldVal, newVal, attrNameLen, indent, path) - if skipped { - result.skippedAttributes++ - } - } - - return true -} - -// 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, 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(sensitiveCaption) - 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, attrWithNestedS *configschema.Attribute, old, new cty.Value, - nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) { - - objS := attrWithNestedS.NestedType - - 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))) - - // Then schema of the attribute itself can be marked sensitive, or the values assigned - sensitive := attrWithNestedS.Sensitive || old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) - if sensitive { - p.buf.WriteString(" = ") - p.buf.WriteString(sensitiveCaption) - - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - return - } - - 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("}") - - if !new.IsKnown() { - p.buf.WriteString(" -> (known after apply)") - } else if new.IsNull() { - p.buf.WriteString(p.color.Color("[dark_gray] -> null[reset]")) - } - - 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)") - } else if new.IsNull() { - p.buf.WriteString(p.color.Color("[dark_gray] -> null[reset]")) - } - - 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)") - } else if new.IsNull() { - p.buf.WriteString(p.color.Color("[dark_gray] -> null[reset]")) - } - - 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)") - } else if new.IsNull() { - p.buf.WriteString(p.color.Color("[dark_gray] -> null[reset]")) - } - } -} - -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 value) - 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 - } - - skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, blankBefore, 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) - } - - blankBeforeInner := blankBefore - 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, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - 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, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - 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, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - 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) - - blankBeforeInner := blankBefore - 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, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - - 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) - - blankBeforeInner := blankBefore - 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, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - } - 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, blankBefore bool, path cty.Path) bool { - if action == plans.NoOp && !p.verbose { - return true - } - - if blankBefore { - p.buf.WriteRune('\n') - } - - 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(sensitiveCaption) - 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)) - displayAttrNames := make(map[string]string, len(atys)) - nameLen := 0 - for attrName := range atys { - attrNames = append(attrNames, attrName) - displayAttrNames[attrName] = displayAttributeName(attrName) - if len(displayAttrNames[attrName]) > nameLen { - nameLen = len(displayAttrNames[attrName]) - } - } - sort.Strings(attrNames) - - for _, attrName := range attrNames { - val := val.GetAttr(attrName) - displayAttrName := displayAttrNames[attrName] - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.buf.WriteString(displayAttrName) - p.buf.WriteString(strings.Repeat(" ", nameLen-len(displayAttrName))) - 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(sensitiveCaption) - 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(cty.StringVal(k), 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(sensitiveCaption) - } 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(sensitiveCaption) - } 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 - displayKeys := make(map[string]string) - keyLen := 0 - for it := old.ElementIterator(); it.Next(); { - k, _ := it.Element() - keyStr := k.AsString() - allKeys = append(allKeys, keyStr) - displayKeys[keyStr] = displayAttributeName(keyStr) - if len(displayKeys[keyStr]) > keyLen { - keyLen = len(displayKeys[keyStr]) - } - } - for it := new.ElementIterator(); it.Next(); { - k, _ := it.Element() - keyStr := k.AsString() - allKeys = append(allKeys, keyStr) - displayKeys[keyStr] = displayAttributeName(keyStr) - if len(displayKeys[keyStr]) > keyLen { - keyLen = len(displayKeys[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 - } - - // TODO: If in future we have a schema associated with this - // object, we should pass the attribute's schema to - // identifyingAttribute here. - if action == plans.NoOp && !p.verbose && !identifyingAttribute(k, nil) { - suppressedElements++ - continue - } - - path := append(path, cty.GetAttrStep{Name: kV}) - - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.buf.WriteString(displayKeys[k]) - p.buf.WriteString(strings.Repeat(" ", keyLen-len(displayKeys[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, objchange.ValueEqual) - 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]" - case plans.NoOp: - return " " - 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") - } -} - -func displayAttributeName(name string) string { - if !hclsyntax.ValidIdentifier(name) { - return fmt.Sprintf("%q", name) - } - return name -} diff --git a/internal/command/format/diff_test.go b/internal/command/format/diff_test.go deleted file mode 100644 index 5ab9502c37..0000000000 --- a/internal/command/format/diff_test.go +++ /dev/null @@ -1,7007 +0,0 @@ -package format - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "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/states" - "github.com/mitchellh/colorstring" - "github.com/zclconf/go-cty/cty" -) - -func TestResourceChange_primitiveTypes(t *testing.T) { - testCases := map[string]testCase{ - "creation": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + id = (known after apply) - } -`, - }, - "creation (null string)": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal("null"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "string": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + string = "null" - } -`, - }, - "creation (null string with extra whitespace)": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "string": cty.StringVal("null "), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "string": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + string = "null " - } -`, - }, - "creation (object with quoted keys)": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "object": cty.ObjectVal(map[string]cty.Value{ - "unquoted": cty.StringVal("value"), - "quoted:key": cty.StringVal("some-value"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "object": {Type: cty.Object(map[string]cty.Type{ - "unquoted": cty.String, - "quoted:key": cty.String, - }), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + object = { - + "quoted:key" = "some-value" - + unquoted = "value" - } - } -`, - }, - "deletion": { - Action: plans.Delete, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - }), - After: cty.NullVal(cty.EmptyObject), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be destroyed - - resource "test_instance" "example" { - - id = "i-02ae66f368e8518a9" -> null - } -`, - }, - "deletion of deposed object": { - Action: plans.Delete, - Mode: addrs.ManagedResourceMode, - DeposedKey: states.DeposedKey("byebye"), - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - }), - After: cty.NullVal(cty.EmptyObject), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example (deposed object byebye) will be destroyed - # (left over from a partially-failed replacement of this instance) - - resource "test_instance" "example" { - - id = "i-02ae66f368e8518a9" -> null - } -`, - }, - "deletion (empty string)": { - Action: plans.Delete, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "intentionally_long": cty.StringVal(""), - }), - After: cty.NullVal(cty.EmptyObject), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "intentionally_long": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be destroyed - - resource "test_instance" "example" { - - id = "i-02ae66f368e8518a9" -> null - } -`, - }, - "string in-place update": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - } -`, - }, - "update with quoted key": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "saml:aud": cty.StringVal("https://example.com/saml"), - "zeta": cty.StringVal("alpha"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "saml:aud": cty.StringVal("https://saml.example.com"), - "zeta": cty.StringVal("alpha"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "saml:aud": {Type: cty.String, Optional: true}, - "zeta": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" - ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com" - # (1 unchanged attribute hidden) - } -`, - }, - "string force-new update": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "ami"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement - id = "i-02ae66f368e8518a9" - } -`, - }, - "string in-place update (null values)": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "unchanged": cty.NullVal(cty.String), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "unchanged": cty.NullVal(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "unchanged": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update of multi-line string field": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "more_lines": cty.StringVal(`original -long -multi-line -string -field -`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "more_lines": cty.StringVal(`original -extremely long -multi-line -string -field -`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "more_lines": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ more_lines = <<-EOT - original - - long - + extremely long - multi-line - string - field - EOT - } -`, - }, - "addition of multi-line string field": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "more_lines": cty.NullVal(cty.String), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "more_lines": cty.StringVal(`original -new line -`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "more_lines": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - + more_lines = <<-EOT - original - new line - EOT - } -`, - }, - "force-new update of multi-line string field": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "more_lines": cty.StringVal(`original -`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "more_lines": cty.StringVal(`original -new line -`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "more_lines": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "more_lines"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ more_lines = <<-EOT # forces replacement - original - + new line - EOT - } -`, - }, - - // Sensitive - - "creation with sensitive field": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "password": cty.StringVal("top-secret"), - "conn_info": cty.ObjectVal(map[string]cty.Value{ - "user": cty.StringVal("not-secret"), - "password": cty.StringVal("top-secret"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "password": {Type: cty.String, Optional: true, Sensitive: true}, - "conn_info": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "user": {Type: cty.String, Optional: true}, - "password": {Type: cty.String, Optional: true, Sensitive: true}, - }, - }, - }, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + conn_info = { - + password = (sensitive value) - + user = "not-secret" - } - + id = (known after apply) - + password = (sensitive value) - } -`, - }, - "update with equal sensitive field": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("blah"), - "str": cty.StringVal("before"), - "password": cty.StringVal("top-secret"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "str": cty.StringVal("after"), - "password": cty.StringVal("top-secret"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "str": {Type: cty.String, Optional: true}, - "password": {Type: cty.String, Optional: true, Sensitive: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "blah" -> (known after apply) - ~ str = "before" -> "after" - # (1 unchanged attribute hidden) - } -`, - }, - - // tainted objects - "replace tainted resource": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseTainted, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-AFTER"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "ami"}, - }), - ExpectedOutput: ` # test_instance.example is tainted, so must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - } -`, - }, - "force replacement with empty before value": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - "forced": cty.NullVal(cty.String), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - "forced": cty.StringVal("example"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - "forced": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "forced"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - + forced = "example" # forces replacement - name = "name" - } -`, - }, - "force replacement with empty before value legacy": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - "forced": cty.StringVal(""), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - "forced": cty.StringVal("example"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - "forced": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "forced"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - + forced = "example" # forces replacement - name = "name" - } -`, - }, - "read during apply because of unknown configuration": { - Action: plans.Read, - ActionReason: plans.ResourceInstanceReadBecauseConfigUnknown, - Mode: addrs.DataResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - }, - }, - ExpectedOutput: ` # data.test_instance.example will be read during apply - # (config refers to values not yet known) - <= data "test_instance" "example" { - name = "name" - } -`, - }, - "read during apply because of pending changes to upstream dependency": { - Action: plans.Read, - ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, - Mode: addrs.DataResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - }, - }, - ExpectedOutput: ` # data.test_instance.example will be read during apply - # (depends on a resource or a module with changes pending) - <= data "test_instance" "example" { - name = "name" - } -`, - }, - "read during apply for unspecified reason": { - Action: plans.Read, - Mode: addrs.DataResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("name"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - }, - }, - ExpectedOutput: ` # data.test_instance.example will be read during apply - <= data "test_instance" "example" { - name = "name" - } -`, - }, - "show all identifying attributes even if unchanged": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "bar": cty.StringVal("bar"), - "foo": cty.StringVal("foo"), - "name": cty.StringVal("alice"), - "tags": cty.MapVal(map[string]cty.Value{ - "name": cty.StringVal("bob"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "bar": cty.StringVal("bar"), - "foo": cty.StringVal("foo"), - "name": cty.StringVal("alice"), - "tags": cty.MapVal(map[string]cty.Value{ - "name": cty.StringVal("bob"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "bar": {Type: cty.String, Optional: true}, - "foo": {Type: cty.String, Optional: true}, - "name": {Type: cty.String, Optional: true}, - "tags": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - name = "alice" - tags = { - "name" = "bob" - } - # (2 unchanged attributes hidden) - } -`, - }, - } - - runTestCases(t, testCases) -} - -func TestResourceChange_JSON(t *testing.T) { - testCases := map[string]testCase{ - "creation": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{ - "str": "value", - "list":["a","b", 234, true], - "obj": {"key": "val"} - }`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + id = (known after apply) - + json_field = jsonencode( - { - + list = [ - + "a", - + "b", - + 234, - + true, - ] - + obj = { - + key = "val" - } - + str = "value" - } - ) - } -`, - }, - "in-place update of object": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - + bbb = "new_value" - - ccc = 5 -> null - # (1 unchanged element hidden) - } - ) - } -`, - }, - "in-place update of object with quoted keys": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": "value", "c:c": "old_value"}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa": "value", "b:bb": "new_value"}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - + "b:bb" = "new_value" - - "c:c" = "old_value" -> null - # (1 unchanged element hidden) - } - ) - } -`, - }, - "in-place update (from empty tuple)": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": []}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa": ["value"]}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - ~ aaa = [ - + "value", - ] - } - ) - } -`, - }, - "in-place update (to empty tuple)": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": ["value"]}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa": []}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - ~ aaa = [ - - "value", - ] - } - ) - } -`, - }, - "in-place update (tuple of different types)": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - ~ aaa = [ - 42, - ~ { - ~ foo = "bar" -> "baz" - }, - "value", - ] - } - ) - } -`, - }, - "force-new update": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": "value"}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "json_field"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - + bbb = "new_value" - # (1 unchanged element hidden) - } # forces replacement - ) - } -`, - }, - "in-place update (whitespace change)": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa":"value", - "bbb":"another"}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( # whitespace changes - { - aaa = "value" - bbb = "another" - } - ) - } -`, - }, - "force-new update (whitespace change)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"aaa":"value", - "bbb":"another"}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "json_field"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( # whitespace changes force replacement - { - aaa = "value" - bbb = "another" - } - ) - } -`, - }, - "creation (empty)": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + id = (known after apply) - + json_field = jsonencode({}) - } -`, - }, - "JSON list item removal": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`["first","second","third"]`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`["first","second"]`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ [ - # (1 unchanged element hidden) - "second", - - "third", - ] - ) - } -`, - }, - "JSON list item addition": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`["first","second"]`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`["first","second","third"]`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ [ - # (1 unchanged element hidden) - "second", - + "third", - ] - ) - } -`, - }, - "JSON list object addition": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"first":"111"}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"first":"111","second":"222"}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - + second = "222" - # (1 unchanged element hidden) - } - ) - } -`, - }, - "JSON object with nested list": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{ - "Statement": ["first"] - }`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{ - "Statement": ["first", "second"] - }`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - ~ Statement = [ - "first", - + "second", - ] - } - ) - } -`, - }, - "JSON list of objects - adding item": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`[{"one": "111"}]`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ [ - { - one = "111" - }, - + { - + two = "222" - }, - ] - ) - } -`, - }, - "JSON list of objects - removing item": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ [ - { - one = "111" - }, - - { - - two = "222" - }, - { - three = "333" - }, - ] - ) - } -`, - }, - "JSON object with list of objects": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - ~ parent = [ - { - one = "111" - }, - + { - + two = "222" - }, - ] - } - ) - } -`, - }, - "JSON object double nested lists": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - ~ parent = [ - ~ { - ~ another_list = [ - "111", - + "222", - ] - }, - ] - } - ) - } -`, - }, - "in-place update from object to tuple": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "json_field": cty.StringVal(`["aaa", 42, "something"]`), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "json_field": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ json_field = jsonencode( - ~ { - - aaa = [ - - 42, - - { - - foo = "bar" - }, - - "value", - ] - } -> [ - + "aaa", - + 42, - + "something", - ] - ) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_listObject(t *testing.T) { - testCases := map[string]testCase{ - // https://github.com/hashicorp/terraform/issues/30641 - "updating non-identifying attribute": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "accounts": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("1"), - "name": cty.StringVal("production"), - "status": cty.StringVal("ACTIVE"), - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("2"), - "name": cty.StringVal("staging"), - "status": cty.StringVal("ACTIVE"), - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("3"), - "name": cty.StringVal("disaster-recovery"), - "status": cty.StringVal("ACTIVE"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "accounts": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("1"), - "name": cty.StringVal("production"), - "status": cty.StringVal("ACTIVE"), - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("2"), - "name": cty.StringVal("staging"), - "status": cty.StringVal("EXPLODED"), - }), - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("3"), - "name": cty.StringVal("disaster-recovery"), - "status": cty.StringVal("ACTIVE"), - }), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "accounts": { - Type: cty.List(cty.Object(map[string]cty.Type{ - "id": cty.String, - "name": cty.String, - "status": cty.String, - })), - }, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ accounts = [ - { - id = "1" - name = "production" - status = "ACTIVE" - }, - ~ { - id = "2" - name = "staging" - ~ status = "ACTIVE" -> "EXPLODED" - }, - { - id = "3" - name = "disaster-recovery" - status = "ACTIVE" - }, - ] - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_primitiveList(t *testing.T) { - testCases := map[string]testCase{ - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.NullVal(cty.List(cty.String)), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("new-element"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - + list_field = [ - + "new-element", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - first addition": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListValEmpty(cty.String), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("new-element"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ - + "new-element", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - cty.StringVal("ffff"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - cty.StringVal("ffff"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ - # (1 unchanged element hidden) - "bbbb", - + "cccc", - "dddd", - # (2 unchanged elements hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - "force-new update - insertion": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "list_field"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ # forces replacement - "aaaa", - + "bbbb", - "cccc", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("bbbb"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ - - "aaaa", - "bbbb", - - "cccc", - "dddd", - # (1 unchanged element hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - "creation - empty list": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + ami = "ami-STATIC" - + id = (known after apply) - + list_field = [] - } -`, - }, - "in-place update - full to empty": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ - - "aaaa", - - "bbbb", - - "cccc", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - null to empty": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.NullVal(cty.List(cty.String)), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - + list_field = [] - # (1 unchanged attribute hidden) - } -`, - }, - "update to unknown element": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.UnknownVal(cty.String), - cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ - "aaaa", - - "bbbb", - + (known after apply), - "cccc", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "update - two new unknown elements": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.UnknownVal(cty.String), - cty.UnknownVal(cty.String), - cty.StringVal("cccc"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ list_field = [ - "aaaa", - - "bbbb", - + (known after apply), - + (known after apply), - "cccc", - # (2 unchanged elements hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_primitiveTuple(t *testing.T) { - testCases := map[string]testCase{ - "in-place update": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "tuple_field": cty.TupleVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("dddd"), - cty.StringVal("eeee"), - cty.StringVal("ffff"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "tuple_field": cty.TupleVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - cty.StringVal("eeee"), - cty.StringVal("ffff"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Required: true}, - "tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" - ~ tuple_field = [ - # (1 unchanged element hidden) - "bbbb", - - "dddd", - + "cccc", - "eeee", - # (1 unchanged element hidden) - ] - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_primitiveSet(t *testing.T) { - testCases := map[string]testCase{ - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.NullVal(cty.Set(cty.String)), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("new-element"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - + set_field = [ - + "new-element", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - first insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetValEmpty(cty.String), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("new-element"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ - + "new-element", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ - + "bbbb", - # (2 unchanged elements hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - "force-new update - insertion": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "set_field"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ # forces replacement - + "bbbb", - # (2 unchanged elements hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("bbbb"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ - - "aaaa", - - "cccc", - # (1 unchanged element hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - "creation - empty set": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + ami = "ami-STATIC" - + id = (known after apply) - + set_field = [] - } -`, - }, - "in-place update - full to empty set": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ - - "aaaa", - - "bbbb", - ] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - null to empty set": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.NullVal(cty.Set(cty.String)), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - + set_field = [] - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update to unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.UnknownVal(cty.Set(cty.String)), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ - - "aaaa", - - "bbbb", - ] -> (known after apply) - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update to unknown element": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.StringVal("bbbb"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "set_field": cty.SetVal([]cty.Value{ - cty.StringVal("aaaa"), - cty.UnknownVal(cty.String), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "set_field": {Type: cty.Set(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ set_field = [ - - "bbbb", - ~ (known after apply), - # (1 unchanged element hidden) - ] - # (1 unchanged attribute hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_map(t *testing.T) { - testCases := map[string]testCase{ - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.NullVal(cty.Map(cty.String)), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "new-key": cty.StringVal("new-element"), - "be:ep": cty.StringVal("boop"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - + map_field = { - + "be:ep" = "boop" - + "new-key" = "new-element" - } - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - first insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapValEmpty(cty.String), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "new-key": cty.StringVal("new-element"), - "be:ep": cty.StringVal("boop"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ map_field = { - + "be:ep" = "boop" - + "new-key" = "new-element" - } - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "c": cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "b": cty.StringVal("bbbb"), - "b:b": cty.StringVal("bbbb"), - "c": cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ map_field = { - + "b" = "bbbb" - + "b:b" = "bbbb" - # (2 unchanged elements hidden) - } - # (1 unchanged attribute hidden) - } -`, - }, - "force-new update - insertion": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "c": cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "b": cty.StringVal("bbbb"), - "c": cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "map_field"}, - }), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ map_field = { # forces replacement - + "b" = "bbbb" - # (2 unchanged elements hidden) - } - # (1 unchanged attribute hidden) - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "b": cty.StringVal("bbbb"), - "c": cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "b": cty.StringVal("bbbb"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ map_field = { - - "a" = "aaaa" -> null - - "c" = "cccc" -> null - # (1 unchanged element hidden) - } - # (1 unchanged attribute hidden) - } -`, - }, - "creation - empty": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapValEmpty(cty.String), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + ami = "ami-STATIC" - + id = (known after apply) - + map_field = {} - } -`, - }, - "update to unknown element": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "b": cty.StringVal("bbbb"), - "c": cty.StringVal("cccc"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.UnknownVal(cty.String), - "ami": cty.StringVal("ami-STATIC"), - "map_field": cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("aaaa"), - "b": cty.UnknownVal(cty.String), - "c": cty.StringVal("cccc"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_field": {Type: cty.Map(cty.String), Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ id = "i-02ae66f368e8518a9" -> (known after apply) - ~ map_field = { - ~ "b" = "bbbb" -> (known after apply) - # (2 unchanged elements hidden) - } - # (1 unchanged attribute hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedList(t *testing.T) { - testCases := map[string]testCase{ - "in-place update - equal": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - })}), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - + { - + mount_point = "/var/diska" - + size = "50GB" - }, - ] - id = "i-02ae66f368e8518a9" - - + root_block_device {} - } -`, - }, - "in-place update - first insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - + { - + mount_point = "/var/diska" - }, - ] - id = "i-02ae66f368e8518a9" - - + root_block_device { - + volume_type = "gp2" - } - } -`, - }, - "in-place update - insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.NullVal(cty.String), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - ~ { - + size = "50GB" - # (1 unchanged attribute hidden) - }, - # (1 unchanged element hidden) - ] - id = "i-02ae66f368e8518a9" - - ~ root_block_device { - + new_field = "new_value" - # (1 unchanged attribute hidden) - } - } -`, - }, - "force-new update (inside blocks)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("different"), - }), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{ - cty.GetAttrStep{Name: "root_block_device"}, - cty.IndexStep{Key: cty.NumberIntVal(0)}, - cty.GetAttrStep{Name: "volume_type"}, - }, - cty.Path{ - cty.GetAttrStep{Name: "disks"}, - cty.IndexStep{Key: cty.NumberIntVal(0)}, - cty.GetAttrStep{Name: "mount_point"}, - }, - ), - Schema: testSchema(configschema.NestingList), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - ~ { - ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement - # (1 unchanged attribute hidden) - }, - ] - id = "i-02ae66f368e8518a9" - - ~ root_block_device { - ~ volume_type = "gp2" -> "different" # forces replacement - } - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("different"), - }), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, - cty.Path{cty.GetAttrStep{Name: "disks"}}, - ), - Schema: testSchema(configschema.NestingList), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ # forces replacement - ~ { - ~ mount_point = "/var/diska" -> "/var/diskb" - # (1 unchanged attribute hidden) - }, - ] - id = "i-02ae66f368e8518a9" - - ~ root_block_device { # forces replacement - ~ volume_type = "gp2" -> "different" - } - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - - { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - ] - id = "i-02ae66f368e8518a9" - - - root_block_device { - - volume_type = "gp2" -> null - } - } -`, - }, - "with dynamically-typed attribute": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "block": cty.EmptyTupleVal, - }), - After: cty.ObjectVal(map[string]cty.Value{ - "block": cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "attr": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "attr": cty.True, - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "block": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "attr": {Type: cty.DynamicPseudoType, Optional: true}, - }, - }, - Nesting: configschema.NestingList, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - + block { - + attr = "foo" - } - + block { - + attr = true - } - } -`, - }, - "in-place sequence update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}), - cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), - cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "list": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "attr": { - Type: cty.String, - Required: true, - }, - }, - }, - Nesting: configschema.NestingList, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ list { - ~ attr = "x" -> "y" - } - ~ list { - ~ attr = "y" -> "z" - } - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - - { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - ] -> (known after apply) - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - modification": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskc"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("75GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskc"), - "size": cty.StringVal("25GB"), - }), - }), - "root_block_device": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - ~ { - ~ size = "50GB" -> "75GB" - # (1 unchanged attribute hidden) - }, - ~ { - ~ size = "50GB" -> "25GB" - # (1 unchanged attribute hidden) - }, - # (1 unchanged element hidden) - ] - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedSet(t *testing.T) { - testCases := map[string]testCase{ - "creation from null - sensitive set": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.Object(map[string]cty.Type{ - "id": cty.String, - "ami": cty.String, - "disks": cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.Set(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - })), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + ami = "ami-AFTER" - + disks = (sensitive value) - + id = "i-02ae66f368e8518a9" - - + root_block_device { - + volume_type = "gp2" - } - } -`, - }, - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - + { - + mount_point = "/var/diska" - }, - ] - id = "i-02ae66f368e8518a9" - - + root_block_device { - + volume_type = "gp2" - } - } -`, - }, - "in-place update - creation - sensitive set": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - # Warning: this attribute value will be marked as sensitive and will not - # display in UI output after applying this change. - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - - + root_block_device { - + volume_type = "gp2" - } - } -`, - }, - "in-place update - marking set sensitive": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - # Warning: this attribute value will be marked as sensitive and will not - # display in UI output after applying this change. The value is unchanged. - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("100GB"), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.NullVal(cty.String), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("100GB"), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - + { - + mount_point = "/var/diska" - + size = "50GB" - }, - - { - - mount_point = "/var/diska" -> null - }, - # (1 unchanged element hidden) - ] - id = "i-02ae66f368e8518a9" - - + root_block_device { - + new_field = "new_value" - + volume_type = "gp2" - } - - root_block_device { - - volume_type = "gp2" -> null - } - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("different"), - }), - }), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, - cty.Path{cty.GetAttrStep{Name: "disks"}}, - ), - Schema: testSchema(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - - { # forces replacement - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - + { # forces replacement - + mount_point = "/var/diskb" - + size = "50GB" - }, - ] - id = "i-02ae66f368e8518a9" - - + root_block_device { # forces replacement - + volume_type = "different" - } - - root_block_device { # forces replacement - - volume_type = "gp2" -> null - } - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - "new_field": cty.String, - })), - "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - - { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - ] - id = "i-02ae66f368e8518a9" - - - root_block_device { - - new_field = "new_value" -> null - - volume_type = "gp2" -> null - } - } -`, - }, - "in-place update - empty nested sets": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - + disks = [ - ] - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - null insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.NullVal(cty.String), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - + disks = [ - + { - + mount_point = "/var/diska" - + size = "50GB" - }, - ] - id = "i-02ae66f368e8518a9" - - + root_block_device { - + new_field = "new_value" - + volume_type = "gp2" - } - - root_block_device { - - volume_type = "gp2" -> null - } - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - "root_block_device": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = [ - - { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - ] -> (known after apply) - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedMap(t *testing.T) { - testCases := map[string]testCase{ - "creation from null": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "ami": cty.NullVal(cty.String), - "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - "root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - }))), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - + ami = "ami-AFTER" - + disks = { - + "disk_a" = { - + mount_point = "/var/diska" - }, - } - + id = "i-02ae66f368e8518a9" - - + root_block_device "a" { - + volume_type = "gp2" - } - } -`, - }, - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - + "disk_a" = { - + mount_point = "/var/diska" - }, - } - id = "i-02ae66f368e8518a9" - - + root_block_device "a" { - + volume_type = "gp2" - } - } -`, - }, - "in-place update - change attr": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.NullVal(cty.String), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - ~ "disk_a" = { - + size = "50GB" - # (1 unchanged attribute hidden) - }, - } - id = "i-02ae66f368e8518a9" - - ~ root_block_device "a" { - + new_field = "new_value" - # (1 unchanged attribute hidden) - } - } -`, - }, - "in-place update - insertion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.NullVal(cty.String), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "disk_2": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/disk2"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.NullVal(cty.String), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - + "disk_2" = { - + mount_point = "/var/disk2" - + size = "50GB" - }, - # (1 unchanged element hidden) - } - id = "i-02ae66f368e8518a9" - - + root_block_device "b" { - + new_field = "new_value" - + volume_type = "gp2" - } - - # (1 unchanged block hidden) - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("standard"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("100GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("different"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("standard"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(cty.Path{ - cty.GetAttrStep{Name: "root_block_device"}, - cty.IndexStep{Key: cty.StringVal("a")}, - }, - cty.Path{cty.GetAttrStep{Name: "disks"}}, - ), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - ~ "disk_a" = { # forces replacement - ~ size = "50GB" -> "100GB" - # (1 unchanged attribute hidden) - }, - } - id = "i-02ae66f368e8518a9" - - ~ root_block_device "a" { # forces replacement - ~ volume_type = "gp2" -> "different" - } - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - "new_field": cty.String, - })), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - - "disk_a" = { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - } - id = "i-02ae66f368e8518a9" - - - root_block_device "a" { - - new_field = "new_value" -> null - - volume_type = "gp2" -> null - } - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - - "disk_a" = { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - }, - } -> (known after apply) - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - insertion sensitive": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}, - cty.IndexStep{Key: cty.StringVal("disk_a")}, - cty.GetAttrStep{Name: "mount_point"}, - }, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = { - + "disk_a" = { - + mount_point = (sensitive value) - + size = "50GB" - }, - } - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - multiple unchanged blocks": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - # (2 unchanged blocks hidden) - } -`, - }, - "in-place update - multiple blocks first changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ root_block_device "b" { - ~ volume_type = "gp2" -> "gp3" - } - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - multiple blocks second changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ root_block_device "a" { - ~ volume_type = "gp2" -> "gp3" - } - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - multiple blocks changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ root_block_device "a" { - ~ volume_type = "gp2" -> "gp3" - } - ~ root_block_device "b" { - ~ volume_type = "gp2" -> "gp3" - } - } -`, - }, - "in-place update - multiple different unchanged blocks": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaMultipleBlocks(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - # (2 unchanged blocks hidden) - } -`, - }, - "in-place update - multiple different blocks first changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaMultipleBlocks(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ leaf_block_device "b" { - ~ volume_type = "gp2" -> "gp3" - } - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - multiple different blocks second changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaMultipleBlocks(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ root_block_device "a" { - ~ volume_type = "gp2" -> "gp3" - } - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - multiple different blocks changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaMultipleBlocks(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ leaf_block_device "b" { - ~ volume_type = "gp2" -> "gp3" - } - - ~ root_block_device "a" { - ~ volume_type = "gp2" -> "gp3" - } - } -`, - }, - "in-place update - mixed blocks unchanged": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaMultipleBlocks(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - # (4 unchanged blocks hidden) - } -`, - }, - "in-place update - mixed blocks changed": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - "root_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - "leaf_block_device": cty.MapVal(map[string]cty.Value{ - "a": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp3"), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaMultipleBlocks(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - ~ leaf_block_device "b" { - ~ volume_type = "gp2" -> "gp3" - } - - ~ root_block_device "b" { - ~ volume_type = "gp2" -> "gp3" - } - - # (2 unchanged blocks hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedSingle(t *testing.T) { - testCases := map[string]testCase{ - "in-place update - equal": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - id = "i-02ae66f368e8518a9" - # (1 unchanged attribute hidden) - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - creation": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - "disk": cty.NullVal(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.NullVal(cty.String), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - + disk = { - + mount_point = "/var/diska" - + size = "50GB" - } - id = "i-02ae66f368e8518a9" - - + root_block_device {} - } -`, - }, - "force-new update (inside blocks)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("different"), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{ - cty.GetAttrStep{Name: "root_block_device"}, - cty.GetAttrStep{Name: "volume_type"}, - }, - cty.Path{ - cty.GetAttrStep{Name: "disk"}, - cty.GetAttrStep{Name: "mount_point"}, - }, - ), - Schema: testSchema(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disk = { - ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement - # (1 unchanged attribute hidden) - } - id = "i-02ae66f368e8518a9" - - ~ root_block_device { - ~ volume_type = "gp2" -> "different" # forces replacement - } - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diskb"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("different"), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, - cty.Path{cty.GetAttrStep{Name: "disk"}}, - ), - Schema: testSchema(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disk = { # forces replacement - ~ mount_point = "/var/diska" -> "/var/diskb" - # (1 unchanged attribute hidden) - } - id = "i-02ae66f368e8518a9" - - ~ root_block_device { # forces replacement - ~ volume_type = "gp2" -> "different" - } - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ - "volume_type": cty.String, - })), - "disk": cty.NullVal(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchema(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - - disk = { - - mount_point = "/var/diska" -> null - - size = "50GB" -> null - } -> null - id = "i-02ae66f368e8518a9" - - - root_block_device { - - volume_type = "gp2" -> null - } - } -`, - }, - "with dynamically-typed attribute": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "block": cty.NullVal(cty.Object(map[string]cty.Type{ - "attr": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "block": cty.ObjectVal(map[string]cty.Value{ - "attr": cty.StringVal("foo"), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "block": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "attr": {Type: cty.DynamicPseudoType, Optional: true}, - }, - }, - Nesting: configschema.NestingSingle, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - + block { - + attr = "foo" - } - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disk = { - ~ mount_point = "/var/diska" -> (known after apply) - ~ size = "50GB" -> (known after apply) - } -> (known after apply) - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - "in-place update - modification": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disk": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("25GB"), - }), - "root_block_device": cty.ObjectVal(map[string]cty.Value{ - "volume_type": cty.StringVal("gp2"), - "new_field": cty.StringVal("new_value"), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaPlus(configschema.NestingSingle), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disk = { - ~ size = "50GB" -> "25GB" - # (1 unchanged attribute hidden) - } - id = "i-02ae66f368e8518a9" - - # (1 unchanged block hidden) - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedMapSensitiveSchema(t *testing.T) { - testCases := map[string]testCase{ - "creation from null": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "ami": cty.NullVal(cty.String), - "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - + ami = "ami-AFTER" - + disks = (sensitive value) - + id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("100GB"), - }), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{cty.GetAttrStep{Name: "disks"}}, - ), - Schema: testSchemaSensitive(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) # forces replacement - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - - disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.MapVal(map[string]cty.Value{ - "disk_a": cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingMap), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedListSensitiveSchema(t *testing.T) { - testCases := map[string]testCase{ - "creation from null": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "ami": cty.NullVal(cty.String), - "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - + ami = "ami-AFTER" - + disks = (sensitive value) - + id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("100GB"), - }), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{cty.GetAttrStep{Name: "disks"}}, - ), - Schema: testSchemaSensitive(configschema.NestingList), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) # forces replacement - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - - disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingList), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_nestedSetSensitiveSchema(t *testing.T) { - testCases := map[string]testCase{ - "creation from null": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.NullVal(cty.String), - "ami": cty.NullVal(cty.String), - "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - + ami = "ami-AFTER" - + disks = (sensitive value) - + id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - })), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.NullVal(cty.String), - }), - }), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "force-new update (whole block)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("100GB"), - }), - }), - }), - RequiredReplace: cty.NewPathSet( - cty.Path{cty.GetAttrStep{Name: "disks"}}, - ), - Schema: testSchemaSensitive(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) # forces replacement - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - deletion": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - - disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - "in-place update - unknown": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "disks": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "mount_point": cty.StringVal("/var/diska"), - "size": cty.StringVal("50GB"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ - "mount_point": cty.String, - "size": cty.String, - }))), - }), - RequiredReplace: cty.NewPathSet(), - Schema: testSchemaSensitive(configschema.NestingSet), - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" - ~ disks = (sensitive value) - id = "i-02ae66f368e8518a9" - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_actionReason(t *testing.T) { - emptySchema := &configschema.Block{} - nullVal := cty.NullVal(cty.EmptyObject) - emptyVal := cty.EmptyObjectVal - - testCases := map[string]testCase{ - "delete for no particular reason": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceChangeNoReason, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be destroyed - - resource "test_instance" "example" {} -`, - }, - "delete because of wrong repetition mode (NoKey)": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, - Mode: addrs.ManagedResourceMode, - InstanceKey: addrs.NoKey, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be destroyed - # (because resource uses count or for_each) - - resource "test_instance" "example" {} -`, - }, - "delete because of wrong repetition mode (IntKey)": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, - Mode: addrs.ManagedResourceMode, - InstanceKey: addrs.IntKey(1), - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example[1] will be destroyed - # (because resource does not use count) - - resource "test_instance" "example" {} -`, - }, - "delete because of wrong repetition mode (StringKey)": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseWrongRepetition, - Mode: addrs.ManagedResourceMode, - InstanceKey: addrs.StringKey("a"), - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example["a"] will be destroyed - # (because resource does not use for_each) - - resource "test_instance" "example" {} -`, - }, - "delete because no resource configuration": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, - ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.NoKey), - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # module.foo.test_instance.example will be destroyed - # (because test_instance.example is not in configuration) - - resource "test_instance" "example" {} -`, - }, - "delete because no module": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseNoModule, - ModuleInst: addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)), - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # module.foo[1].test_instance.example will be destroyed - # (because module.foo[1] is not in configuration) - - resource "test_instance" "example" {} -`, - }, - "delete because out of range for count": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseCountIndex, - Mode: addrs.ManagedResourceMode, - InstanceKey: addrs.IntKey(1), - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example[1] will be destroyed - # (because index [1] is out of range for count) - - resource "test_instance" "example" {} -`, - }, - "delete because out of range for for_each": { - Action: plans.Delete, - ActionReason: plans.ResourceInstanceDeleteBecauseEachKey, - Mode: addrs.ManagedResourceMode, - InstanceKey: addrs.StringKey("boop"), - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example["boop"] will be destroyed - # (because key ["boop"] is not in for_each map) - - resource "test_instance" "example" {} -`, - }, - "replace for no particular reason (delete first)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceChangeNoReason, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" {} -`, - }, - "replace for no particular reason (create first)": { - Action: plans.CreateThenDelete, - ActionReason: plans.ResourceInstanceChangeNoReason, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example must be replaced -+/- resource "test_instance" "example" {} -`, - }, - "replace by request (delete first)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceByRequest, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be replaced, as requested --/+ resource "test_instance" "example" {} -`, - }, - "replace by request (create first)": { - Action: plans.CreateThenDelete, - ActionReason: plans.ResourceInstanceReplaceByRequest, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be replaced, as requested -+/- resource "test_instance" "example" {} -`, - }, - "replace because tainted (delete first)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseTainted, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example is tainted, so must be replaced --/+ resource "test_instance" "example" {} -`, - }, - "replace because tainted (create first)": { - Action: plans.CreateThenDelete, - ActionReason: plans.ResourceInstanceReplaceBecauseTainted, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example is tainted, so must be replaced -+/- resource "test_instance" "example" {} -`, - }, - "replace because cannot update (delete first)": { - Action: plans.DeleteThenCreate, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - // This one has no special message, because the fuller explanation - // typically appears inline as a "# forces replacement" comment. - // (not shown here) - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" {} -`, - }, - "replace because cannot update (create first)": { - Action: plans.CreateThenDelete, - ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, - Mode: addrs.ManagedResourceMode, - Before: emptyVal, - After: nullVal, - Schema: emptySchema, - RequiredReplace: cty.NewPathSet(), - // This one has no special message, because the fuller explanation - // typically appears inline as a "# forces replacement" comment. - // (not shown here) - ExpectedOutput: ` # test_instance.example must be replaced -+/- resource "test_instance" "example" {} -`, - }, - } - - runTestCases(t, testCases) -} - -func TestResourceChange_sensitiveVariable(t *testing.T) { - testCases := map[string]testCase{ - "creation": { - Action: plans.Create, - Mode: addrs.ManagedResourceMode, - Before: cty.NullVal(cty.EmptyObject), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-123"), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - cty.StringVal("!"), - }), - "nested_block_list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - "another": cty.StringVal("not secret"), - }), - }), - "nested_block_set": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - "another": cty.StringVal("not secret"), - }), - }), - }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - // Nested blocks/sets will mark the whole set/block as sensitive - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "map_whole": {Type: cty.Map(cty.String), Optional: true}, - "map_key": {Type: cty.Map(cty.Number), Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block_list": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - "another": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingList, - }, - "nested_block_set": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - "another": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingSet, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be created - + resource "test_instance" "example" { - + ami = (sensitive value) - + id = "i-02ae66f368e8518a9" - + list_field = [ - + "hello", - + (sensitive value), - + "!", - ] - + map_key = { - + "breakfast" = 800 - + "dinner" = (sensitive value) - } - + map_whole = (sensitive value) - - + nested_block_list { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - - + nested_block_set { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "in-place update - before sensitive": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "special": cty.BoolVal(true), - "some_number": cty.NumberIntVal(1), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - cty.StringVal("!"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - }), - }), - "nested_block_set": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "special": cty.BoolVal(false), - "some_number": cty.NumberIntVal(2), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - cty.StringVal("."), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(1900), - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("cereal"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("changed"), - }), - }), - "nested_block_set": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("changed"), - }), - }), - }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "special"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - "special": {Type: cty.Bool, Optional: true}, - "some_number": {Type: cty.Number, Optional: true}, - "map_key": {Type: cty.Map(cty.Number), Optional: true}, - "map_whole": {Type: cty.Map(cty.String), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingList, - }, - "nested_block_set": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingSet, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. - ~ ami = (sensitive value) - id = "i-02ae66f368e8518a9" - ~ list_field = [ - # (1 unchanged element hidden) - "friends", - - (sensitive value), - + ".", - ] - ~ map_key = { - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. - ~ "dinner" = (sensitive value) - # (1 unchanged element hidden) - } - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. - ~ map_whole = (sensitive value) - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. - ~ some_number = (sensitive value) - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. - ~ special = (sensitive value) - - # Warning: this block will no longer be marked as sensitive - # after applying this change. - ~ nested_block { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - - # Warning: this block will no longer be marked as sensitive - # after applying this change. - ~ nested_block_set { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "in-place update - after sensitive": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block_single": cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("original"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("goodbye"), - cty.StringVal("friends"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(700), - "dinner": cty.NumberIntVal(2100), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("cereal"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block_single": cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("changed"), - }), - }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - "map_key": {Type: cty.Map(cty.Number), Optional: true}, - "map_whole": {Type: cty.Map(cty.String), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block_single": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingSingle, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" - ~ list_field = [ - - "hello", - + (sensitive value), - "friends", - ] - ~ map_key = { - ~ "breakfast" = 800 -> 700 - # Warning: this attribute value will be marked as sensitive and will not - # display in UI output after applying this change. - ~ "dinner" = (sensitive value) - } - # Warning: this attribute value will be marked as sensitive and will not - # display in UI output after applying this change. - ~ map_whole = (sensitive value) - - # Warning: this block will be marked as sensitive and will not - # display in UI output after applying this change. - ~ nested_block_single { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "in-place update - both sensitive": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block_map": cty.MapVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("original"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("goodbye"), - cty.StringVal("friends"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(1800), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("cereal"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block_map": cty.MapVal(map[string]cty.Value{ - "foo": cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.UnknownVal(cty.String), - }), - }), - }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - "map_key": {Type: cty.Map(cty.Number), Optional: true}, - "map_whole": {Type: cty.Map(cty.String), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block_map": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingMap, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - ~ ami = (sensitive value) - id = "i-02ae66f368e8518a9" - ~ list_field = [ - - (sensitive value), - + (sensitive value), - "friends", - ] - ~ map_key = { - ~ "dinner" = (sensitive value) - # (1 unchanged element hidden) - } - ~ map_whole = (sensitive value) - - ~ nested_block_map { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "in-place update - value unchanged, sensitivity changes": { - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "special": cty.BoolVal(true), - "some_number": cty.NumberIntVal(1), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - cty.StringVal("!"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - }), - }), - "nested_block_set": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "special": cty.BoolVal(true), - "some_number": cty.NumberIntVal(1), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - cty.StringVal("!"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - }), - }), - "nested_block_set": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secretval"), - }), - }), - }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "special"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - "special": {Type: cty.Bool, Optional: true}, - "some_number": {Type: cty.Number, Optional: true}, - "map_key": {Type: cty.Map(cty.Number), Optional: true}, - "map_whole": {Type: cty.Map(cty.String), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingList, - }, - "nested_block_set": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingSet, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be updated in-place - ~ resource "test_instance" "example" { - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. The value is unchanged. - ~ ami = (sensitive value) - id = "i-02ae66f368e8518a9" - ~ list_field = [ - # (1 unchanged element hidden) - "friends", - - (sensitive value), - + "!", - ] - ~ map_key = { - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. The value is unchanged. - ~ "dinner" = (sensitive value) - # (1 unchanged element hidden) - } - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. The value is unchanged. - ~ map_whole = (sensitive value) - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. The value is unchanged. - ~ some_number = (sensitive value) - # Warning: this attribute value will no longer be marked as sensitive - # after applying this change. The value is unchanged. - ~ special = (sensitive value) - - # Warning: this block will no longer be marked as sensitive - # after applying this change. - ~ nested_block { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - - # Warning: this block will no longer be marked as sensitive - # after applying this change. - ~ nested_block_set { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "deletion": { - Action: plans.Delete, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "list_field": cty.ListVal([]cty.Value{ - cty.StringVal("hello"), - cty.StringVal("friends"), - }), - "map_key": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.NumberIntVal(800), - "dinner": cty.NumberIntVal(2000), // sensitive key - }), - "map_whole": cty.MapVal(map[string]cty.Value{ - "breakfast": cty.StringVal("pizza"), - "dinner": cty.StringVal("pizza"), - }), - "nested_block": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secret"), - "another": cty.StringVal("not secret"), - }), - }), - "nested_block_set": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secret"), - "another": cty.StringVal("not secret"), - }), - }), - }), - After: cty.NullVal(cty.EmptyObject), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - RequiredReplace: cty.NewPathSet(), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "list_field": {Type: cty.List(cty.String), Optional: true}, - "map_key": {Type: cty.Map(cty.Number), Optional: true}, - "map_whole": {Type: cty.Map(cty.String), Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block_set": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Optional: true}, - "another": {Type: cty.String, Optional: true}, - }, - }, - Nesting: configschema.NestingSet, - }, - }, - }, - ExpectedOutput: ` # test_instance.example will be destroyed - - resource "test_instance" "example" { - - ami = (sensitive value) -> null - - id = "i-02ae66f368e8518a9" -> null - - list_field = [ - - "hello", - - (sensitive value), - ] -> null - - map_key = { - - "breakfast" = 800 - - "dinner" = (sensitive value) - } -> null - - map_whole = (sensitive value) -> null - - - nested_block_set { - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "update with sensitive value forcing replacement": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - "nested_block_set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("secret"), - }), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - "nested_block_set": cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "an_attr": cty.StringVal("changed"), - }), - }), - }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("ami"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nested_block_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("ami"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nested_block_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested_block_set": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "an_attr": {Type: cty.String, Required: true}, - }, - }, - Nesting: configschema.NestingSet, - }, - }, - }, - RequiredReplace: cty.NewPathSet( - cty.GetAttrPath("ami"), - cty.GetAttrPath("nested_block_set"), - ), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = (sensitive value) # forces replacement - id = "i-02ae66f368e8518a9" - - ~ nested_block_set { # forces replacement - # At least one attribute in this block is (or was) sensitive, - # so its contents will not be displayed. - } - } -`, - }, - "update with sensitive attribute forcing replacement": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-BEFORE"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "ami": cty.StringVal("ami-AFTER"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true}, - }, - }, - RequiredReplace: cty.NewPathSet( - cty.GetAttrPath("ami"), - ), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ ami = (sensitive value) # forces replacement - id = "i-02ae66f368e8518a9" - } -`, - }, - "update with sensitive nested type attribute forcing replacement": { - Action: plans.DeleteThenCreate, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "conn_info": cty.ObjectVal(map[string]cty.Value{ - "user": cty.StringVal("not-secret"), - "password": cty.StringVal("top-secret"), - }), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "conn_info": cty.ObjectVal(map[string]cty.Value{ - "user": cty.StringVal("not-secret"), - "password": cty.StringVal("new-secret"), - }), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "conn_info": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "user": {Type: cty.String, Optional: true}, - "password": {Type: cty.String, Optional: true, Sensitive: true}, - }, - }, - }, - }, - }, - RequiredReplace: cty.NewPathSet( - cty.GetAttrPath("conn_info"), - cty.GetAttrPath("password"), - ), - ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" { - ~ conn_info = { # forces replacement - ~ password = (sensitive value) - # (1 unchanged attribute hidden) - } - id = "i-02ae66f368e8518a9" - } -`, - }, - } - runTestCases(t, testCases) -} - -func TestResourceChange_moved(t *testing.T) { - prevRunAddr := addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_instance", - Name: "previous", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - - testCases := map[string]testCase{ - "moved and updated": { - PrevRunAddr: prevRunAddr, - Action: plans.Update, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("12345"), - "foo": cty.StringVal("hello"), - "bar": cty.StringVal("baz"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("12345"), - "foo": cty.StringVal("hello"), - "bar": cty.StringVal("boop"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "foo": {Type: cty.String, Optional: true}, - "bar": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.example will be updated in-place - # (moved from test_instance.previous) - ~ resource "test_instance" "example" { - ~ bar = "baz" -> "boop" - id = "12345" - # (1 unchanged attribute hidden) - } -`, - }, - "moved without changes": { - PrevRunAddr: prevRunAddr, - Action: plans.NoOp, - Mode: addrs.ManagedResourceMode, - Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("12345"), - "foo": cty.StringVal("hello"), - "bar": cty.StringVal("baz"), - }), - After: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("12345"), - "foo": cty.StringVal("hello"), - "bar": cty.StringVal("baz"), - }), - Schema: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "foo": {Type: cty.String, Optional: true}, - "bar": {Type: cty.String, Optional: true}, - }, - }, - RequiredReplace: cty.NewPathSet(), - ExpectedOutput: ` # test_instance.previous has moved to test_instance.example - resource "test_instance" "example" { - id = "12345" - # (2 unchanged attributes hidden) - } -`, - }, - } - - runTestCases(t, testCases) -} - -type testCase struct { - Action plans.Action - ActionReason plans.ResourceInstanceChangeActionReason - ModuleInst addrs.ModuleInstance - Mode addrs.ResourceMode - InstanceKey addrs.InstanceKey - DeposedKey states.DeposedKey - Before cty.Value - BeforeValMarks []cty.PathValueMarks - AfterValMarks []cty.PathValueMarks - After cty.Value - Schema *configschema.Block - RequiredReplace cty.PathSet - ExpectedOutput string - PrevRunAddr addrs.AbsResourceInstance -} - -func runTestCases(t *testing.T, testCases map[string]testCase) { - color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - ty := tc.Schema.ImpliedType() - - beforeVal := tc.Before - switch { // Some fixups to make the test cases a little easier to write - case beforeVal.IsNull(): - beforeVal = cty.NullVal(ty) // allow mistyped nulls - case !beforeVal.IsKnown(): - beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns - } - - afterVal := tc.After - switch { // Some fixups to make the test cases a little easier to write - case afterVal.IsNull(): - afterVal = cty.NullVal(ty) // allow mistyped nulls - case !afterVal.IsKnown(): - afterVal = cty.UnknownVal(ty) // allow mistyped unknowns - } - - addr := addrs.Resource{ - Mode: tc.Mode, - Type: "test_instance", - Name: "example", - }.Instance(tc.InstanceKey).Absolute(tc.ModuleInst) - - prevRunAddr := tc.PrevRunAddr - // If no previous run address is given, reuse the current address - // to make initialization easier - if prevRunAddr.Resource.Resource.Type == "" { - prevRunAddr = addr - } - - change := &plans.ResourceInstanceChange{ - Addr: addr, - PrevRunAddr: prevRunAddr, - DeposedKey: tc.DeposedKey, - ProviderAddr: addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - Change: plans.Change{ - Action: tc.Action, - Before: beforeVal.MarkWithPaths(tc.BeforeValMarks), - After: afterVal.MarkWithPaths(tc.AfterValMarks), - }, - ActionReason: tc.ActionReason, - RequiredReplace: tc.RequiredReplace, - } - - output := ResourceChange(change, tc.Schema, color, DiffLanguageProposedChange) - if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" { - t.Errorf("wrong output\n%s", diff) - } - }) - } -} - -func TestOutputChanges(t *testing.T) { - color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} - - testCases := map[string]struct { - changes []*plans.OutputChangeSrc - output string - }{ - "new output value": { - []*plans.OutputChangeSrc{ - outputChange( - "foo", - cty.NullVal(cty.DynamicPseudoType), - cty.StringVal("bar"), - false, - ), - }, - ` - + foo = "bar"`, - }, - "removed output": { - []*plans.OutputChangeSrc{ - outputChange( - "foo", - cty.StringVal("bar"), - cty.NullVal(cty.DynamicPseudoType), - false, - ), - }, - ` - - foo = "bar" -> null`, - }, - "single string change": { - []*plans.OutputChangeSrc{ - outputChange( - "foo", - cty.StringVal("bar"), - cty.StringVal("baz"), - false, - ), - }, - ` - ~ foo = "bar" -> "baz"`, - }, - "element added to list": { - []*plans.OutputChangeSrc{ - outputChange( - "foo", - cty.ListVal([]cty.Value{ - cty.StringVal("alpha"), - cty.StringVal("beta"), - cty.StringVal("delta"), - cty.StringVal("epsilon"), - }), - cty.ListVal([]cty.Value{ - cty.StringVal("alpha"), - cty.StringVal("beta"), - cty.StringVal("gamma"), - cty.StringVal("delta"), - cty.StringVal("epsilon"), - }), - false, - ), - }, - ` - ~ foo = [ - # (1 unchanged element hidden) - "beta", - + "gamma", - "delta", - # (1 unchanged element hidden) - ]`, - }, - "multiple outputs changed, one sensitive": { - []*plans.OutputChangeSrc{ - outputChange( - "a", - cty.NumberIntVal(1), - cty.NumberIntVal(2), - false, - ), - outputChange( - "b", - cty.StringVal("hunter2"), - cty.StringVal("correct-horse-battery-staple"), - true, - ), - outputChange( - "c", - cty.BoolVal(false), - cty.BoolVal(true), - false, - ), - }, - ` - ~ a = 1 -> 2 - ~ b = (sensitive value) - ~ c = false -> true`, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - output := OutputChanges(tc.changes, color) - if output != tc.output { - t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) - } - }) - } -} - -func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { - addr := addrs.AbsOutputValue{ - OutputValue: addrs.OutputValue{Name: name}, - } - - change := &plans.OutputChange{ - Addr: addr, Change: plans.Change{ - Before: before, - After: after, - }, - Sensitive: sensitive, - } - - changeSrc, err := change.Encode() - if err != nil { - panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err)) - } - - return changeSrc -} - -// A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block -func testSchema(nesting configschema.NestingMode) *configschema.Block { - var diskKey = "disks" - if nesting == configschema.NestingSingle { - diskKey = "disk" - } - - return &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - diskKey: { - NestedType: &configschema.Object{ - Attributes: map[string]*configschema.Attribute{ - "mount_point": {Type: cty.String, Optional: true}, - "size": {Type: cty.String, Optional: true}, - }, - Nesting: nesting, - }, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "root_block_device": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "volume_type": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - Nesting: nesting, - }, - }, - } -} - -// A basic test schema using a configurable NestingMode for one (NestedType) -// attribute marked sensitive. -func testSchemaSensitive(nesting configschema.NestingMode) *configschema.Block { - return &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "disks": { - Sensitive: true, - NestedType: &configschema.Object{ - Attributes: map[string]*configschema.Attribute{ - "mount_point": {Type: cty.String, Optional: true}, - "size": {Type: cty.String, Optional: true}, - }, - Nesting: nesting, - }, - }, - }, - } -} - -func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block { - return &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "disks": { - NestedType: &configschema.Object{ - Attributes: map[string]*configschema.Attribute{ - "mount_point": {Type: cty.String, Optional: true}, - "size": {Type: cty.String, Optional: true}, - }, - Nesting: nesting, - }, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "root_block_device": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "volume_type": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - Nesting: nesting, - }, - "leaf_block_device": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "volume_type": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - Nesting: nesting, - }, - }, - } -} - -// similar to testSchema with the addition of a "new_field" block -func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { - var diskKey = "disks" - if nesting == configschema.NestingSingle { - diskKey = "disk" - } - - return &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - diskKey: { - NestedType: &configschema.Object{ - Attributes: map[string]*configschema.Attribute{ - "mount_point": {Type: cty.String, Optional: true}, - "size": {Type: cty.String, Optional: true}, - }, - Nesting: nesting, - }, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "root_block_device": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "volume_type": { - Type: cty.String, - Optional: true, - Computed: true, - }, - "new_field": { - Type: cty.String, - Optional: true, - Computed: true, - }, - }, - }, - Nesting: nesting, - }, - }, - } -} diff --git a/internal/command/format/difflanguage_string.go b/internal/command/format/difflanguage_string.go deleted file mode 100644 index 8399cddc46..0000000000 --- a/internal/command/format/difflanguage_string.go +++ /dev/null @@ -1,29 +0,0 @@ -// Code generated by "stringer -type=DiffLanguage diff.go"; DO NOT EDIT. - -package format - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[DiffLanguageProposedChange-80] - _ = x[DiffLanguageDetectedDrift-68] -} - -const ( - _DiffLanguage_name_0 = "DiffLanguageDetectedDrift" - _DiffLanguage_name_1 = "DiffLanguageProposedChange" -) - -func (i DiffLanguage) String() string { - switch { - case i == 68: - return _DiffLanguage_name_0 - case i == 80: - return _DiffLanguage_name_1 - default: - return "DiffLanguage(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/internal/command/format/format.go b/internal/command/format/format.go index aa8d7deb2a..6b662bae8a 100644 --- a/internal/command/format/format.go +++ b/internal/command/format/format.go @@ -6,3 +6,30 @@ // output formatting as much as possible so that text formats of Terraform // structures have a consistent look and feel. package format + +import "github.com/hashicorp/terraform/internal/plans" + +// 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]" + case plans.NoOp: + return " " + default: + return " ?" + } +} diff --git a/internal/command/format/state.go b/internal/command/format/state.go deleted file mode 100644 index d0db1cc3dd..0000000000 --- a/internal/command/format/state.go +++ /dev/null @@ -1,216 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "sort" - "strings" - - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/terraform" - "github.com/mitchellh/colorstring" -) - -// StateOpts are the options for formatting a state. -type StateOpts struct { - // State is the state to format. This is required. - State *states.State - - // Schemas are used to decode attributes. This is required. - Schemas *terraform.Schemas - - // Color is the colorizer. This is optional. - Color *colorstring.Colorize -} - -// State takes a state and returns a string -func State(opts *StateOpts) string { - if opts.Color == nil { - panic("colorize not given") - } - - if opts.Schemas == nil { - panic("schemas not given") - } - - s := opts.State - if len(s.Modules) == 0 { - return "The state file is empty. No resources are represented." - } - - buf := bytes.NewBufferString("[reset]") - p := blockBodyDiffPrinter{ - buf: buf, - color: opts.Color, - action: plans.NoOp, - verbose: true, - } - - // Format all the modules - for _, m := range s.Modules { - formatStateModule(p, m, opts.Schemas) - } - - // Write the outputs for the root module - m := s.RootModule() - - if m.OutputValues != nil { - if len(m.OutputValues) > 0 { - p.buf.WriteString("Outputs:\n\n") - } - - // Sort the outputs - ks := make([]string, 0, len(m.OutputValues)) - for k := range m.OutputValues { - ks = append(ks, k) - } - sort.Strings(ks) - - // Output each output k/v pair - for _, k := range ks { - v := m.OutputValues[k] - p.buf.WriteString(fmt.Sprintf("%s = ", k)) - if v.Sensitive { - p.buf.WriteString("(sensitive value)") - } else { - p.writeValue(v.Value, plans.NoOp, 0) - } - p.buf.WriteString("\n") - } - } - - trimmedOutput := strings.TrimSpace(p.buf.String()) - trimmedOutput += "[reset]" - - return opts.Color.Color(trimmedOutput) - -} - -func formatStateModule(p blockBodyDiffPrinter, m *states.Module, schemas *terraform.Schemas) { - // First get the names of all the resources so we can show them - // in alphabetical order. - names := make([]string, 0, len(m.Resources)) - for name := range m.Resources { - names = append(names, name) - } - sort.Strings(names) - - // Go through each resource and begin building up the output. - for _, key := range names { - for k, v := range m.Resources[key].Instances { - // keep these in order to keep the current object first, and - // provide deterministic output for the deposed objects - type obj struct { - header string - instance *states.ResourceInstanceObjectSrc - } - instances := []obj{} - - addr := m.Resources[key].Addr - resAddr := addr.Resource - - taintStr := "" - if v.Current != nil && v.Current.Status == 'T' { - taintStr = " (tainted)" - } - - instances = append(instances, - obj{fmt.Sprintf("# %s:%s\n", addr.Instance(k), taintStr), v.Current}) - - for dk, v := range v.Deposed { - instances = append(instances, - obj{fmt.Sprintf("# %s: (deposed object %s)\n", addr.Instance(k), dk), v}) - } - - // Sort the instances for consistent output. - // Starting the sort from the second index, so the current instance - // is always first. - sort.Slice(instances[1:], func(i, j int) bool { - return instances[i+1].header < instances[j+1].header - }) - - for _, obj := range instances { - header := obj.header - instance := obj.instance - p.buf.WriteString(header) - if instance == nil { - // this shouldn't happen, but there's nothing to do here so - // don't panic below. - continue - } - - var schema *configschema.Block - - provider := m.Resources[key].ProviderConfig.Provider - if _, exists := schemas.Providers[provider]; !exists { - // This should never happen in normal use because we should've - // loaded all of the schemas and checked things prior to this - // point. We can't return errors here, but since this is UI code - // we will try to do _something_ reasonable. - p.buf.WriteString(fmt.Sprintf("# missing schema for provider %q\n\n", provider.String())) - continue - } - - switch resAddr.Mode { - case addrs.ManagedResourceMode: - schema, _ = schemas.ResourceTypeConfig( - provider, - resAddr.Mode, - resAddr.Type, - ) - if schema == nil { - p.buf.WriteString(fmt.Sprintf( - "# missing schema for provider %q resource type %s\n\n", provider, resAddr.Type)) - continue - } - - p.buf.WriteString(fmt.Sprintf( - "resource %q %q {", - resAddr.Type, - resAddr.Name, - )) - case addrs.DataResourceMode: - schema, _ = schemas.ResourceTypeConfig( - provider, - resAddr.Mode, - resAddr.Type, - ) - if schema == nil { - p.buf.WriteString(fmt.Sprintf( - "# missing schema for provider %q data source %s\n\n", provider, resAddr.Type)) - continue - } - - p.buf.WriteString(fmt.Sprintf( - "data %q %q {", - resAddr.Type, - resAddr.Name, - )) - default: - // should never happen, since the above is exhaustive - p.buf.WriteString(resAddr.String()) - } - - val, err := instance.Decode(schema.ImpliedType()) - if err != nil { - fmt.Println(err.Error()) - break - } - - path := make(cty.Path, 0, 3) - result := p.writeBlockBodyDiff(schema, val.Value, val.Value, 2, path) - if result.bodyWritten { - p.buf.WriteString("\n") - } - - p.buf.WriteString("}\n\n") - } - } - } - p.buf.WriteString("\n") -} diff --git a/internal/command/format/state_test.go b/internal/command/format/state_test.go deleted file mode 100644 index d83c6eaf97..0000000000 --- a/internal/command/format/state_test.go +++ /dev/null @@ -1,400 +0,0 @@ -package format - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/terraform" - "github.com/zclconf/go-cty/cty" -) - -func TestState(t *testing.T) { - tests := []struct { - State *StateOpts - Want string - }{ - { - &StateOpts{ - State: &states.State{}, - Color: disabledColorize, - Schemas: &terraform.Schemas{}, - }, - "The state file is empty. No resources are represented.", - }, - { - &StateOpts{ - State: basicState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - basicStateOutput, - }, - { - &StateOpts{ - State: nestedState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - nestedStateOutput, - }, - { - &StateOpts{ - State: deposedState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - deposedNestedStateOutput, - }, - { - &StateOpts{ - State: onlyDeposedState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - onlyDeposedOutput, - }, - { - &StateOpts{ - State: stateWithMoreOutputs(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - stateWithMoreOutputsOutput, - }, - } - - for i, tt := range tests { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - got := State(tt.State) - if got != tt.Want { - t.Errorf( - "wrong result\ninput: %v\ngot: \n%q\nwant: \n%q", - tt.State.State, got, tt.Want, - ) - } - }) - } -} - -func testProvider() *terraform.MockProvider { - p := new(terraform.MockProvider) - p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { - return providers.ReadResourceResponse{NewState: req.PriorState} - } - - p.GetProviderSchemaResponse = testProviderSchema() - - return p -} - -func testProviderSchema() *providers.GetProviderSchemaResponse { - return &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{ - Block: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "region": {Type: cty.String, Optional: true}, - }, - }, - }, - ResourceTypes: map[string]providers.Schema{ - "test_resource": { - Block: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "foo": {Type: cty.String, Optional: true}, - "woozles": {Type: cty.String, Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nested": { - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "compute": {Type: cty.String, Optional: true}, - "value": {Type: cty.String, Optional: true}, - }, - }, - }, - }, - }, - }, - }, - DataSources: map[string]providers.Schema{ - "test_data_source": { - Block: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "compute": {Type: cty.String, Optional: true}, - "value": {Type: cty.String, Computed: true}, - }, - }, - }, - }, - } -} - -func testSchemas() *terraform.Schemas { - provider := testProvider() - return &terraform.Schemas{ - Providers: map[addrs.Provider]*terraform.ProviderSchema{ - addrs.NewDefaultProvider("test"): provider.ProviderSchema(), - }, - } -} - -const basicStateOutput = `# data.test_data_source.data: -data "test_data_source" "data" { - compute = "sure" -} - -# test_resource.baz[0]: -resource "test_resource" "baz" { - woozles = "confuzles" -} - - -Outputs: - -bar = "bar value"` - -const nestedStateOutput = `# test_resource.baz[0]: -resource "test_resource" "baz" { - woozles = "confuzles" - - nested { - value = "42" - } -}` - -const deposedNestedStateOutput = `# test_resource.baz[0]: -resource "test_resource" "baz" { - woozles = "confuzles" - - nested { - value = "42" - } -} - -# test_resource.baz[0]: (deposed object 1234) -resource "test_resource" "baz" { - woozles = "confuzles" - - nested { - value = "42" - } -}` - -const onlyDeposedOutput = `# test_resource.baz[0]: -# test_resource.baz[0]: (deposed object 1234) -resource "test_resource" "baz" { - woozles = "confuzles" - - nested { - value = "42" - } -} - -# test_resource.baz[0]: (deposed object 5678) -resource "test_resource" "baz" { - woozles = "confuzles" - - nested { - value = "42" - } -}` - -const stateWithMoreOutputsOutput = `# test_resource.baz[0]: -resource "test_resource" "baz" { - woozles = "confuzles" -} - - -Outputs: - -bool_var = true -int_var = 42 -map_var = { - "first" = "foo" - "second" = "bar" -} -sensitive_var = (sensitive value) -string_var = "string value"` - -func basicState(t *testing.T) *states.State { - state := states.NewState() - - rootModule := state.RootModule() - if rootModule == nil { - t.Errorf("root module is nil; want valid object") - } - - rootModule.SetLocalValue("foo", cty.StringVal("foo value")) - rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false) - rootModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_resource", - Name: "baz", - }.Instance(addrs.IntKey(0)), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"woozles":"confuzles"}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - rootModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.DataResourceMode, - Type: "test_data_source", - Name: "data", - }.Instance(addrs.NoKey), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"compute":"sure"}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - return state -} - -func stateWithMoreOutputs(t *testing.T) *states.State { - state := states.NewState() - - rootModule := state.RootModule() - if rootModule == nil { - t.Errorf("root module is nil; want valid object") - } - - rootModule.SetOutputValue("string_var", cty.StringVal("string value"), false) - rootModule.SetOutputValue("int_var", cty.NumberIntVal(42), false) - rootModule.SetOutputValue("bool_var", cty.BoolVal(true), false) - rootModule.SetOutputValue("sensitive_var", cty.StringVal("secret!!!"), true) - rootModule.SetOutputValue("map_var", cty.MapVal(map[string]cty.Value{ - "first": cty.StringVal("foo"), - "second": cty.StringVal("bar"), - }), false) - - rootModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_resource", - Name: "baz", - }.Instance(addrs.IntKey(0)), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"woozles":"confuzles"}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - return state -} - -func nestedState(t *testing.T) *states.State { - state := states.NewState() - - rootModule := state.RootModule() - if rootModule == nil { - t.Errorf("root module is nil; want valid object") - } - - rootModule.SetResourceInstanceCurrent( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_resource", - Name: "baz", - }.Instance(addrs.IntKey(0)), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - return state -} - -func deposedState(t *testing.T) *states.State { - state := nestedState(t) - rootModule := state.RootModule() - rootModule.SetResourceInstanceDeposed( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_resource", - Name: "baz", - }.Instance(addrs.IntKey(0)), - states.DeposedKey("1234"), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - return state -} - -// replicate a corrupt resource where only a deposed exists -func onlyDeposedState(t *testing.T) *states.State { - state := states.NewState() - - rootModule := state.RootModule() - if rootModule == nil { - t.Errorf("root module is nil; want valid object") - } - - rootModule.SetResourceInstanceDeposed( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_resource", - Name: "baz", - }.Instance(addrs.IntKey(0)), - states.DeposedKey("1234"), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - rootModule.SetResourceInstanceDeposed( - addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_resource", - Name: "baz", - }.Instance(addrs.IntKey(0)), - states.DeposedKey("5678"), - &states.ResourceInstanceObjectSrc{ - Status: states.ObjectReady, - SchemaVersion: 1, - AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), - }, - addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - ) - return state -} diff --git a/internal/command/jsonformat/state_test.go b/internal/command/jsonformat/state_test.go index 1728f63a5a..3a0d40ee43 100644 --- a/internal/command/jsonformat/state_test.go +++ b/internal/command/jsonformat/state_test.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/mitchellh/colorstring" - "github.com/hashicorp/terraform/internal/command/format" "github.com/hashicorp/terraform/internal/command/jsonprovider" "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/states/statefile" @@ -26,56 +25,39 @@ func TestState(t *testing.T) { color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} tests := []struct { - State *format.StateOpts - Want string + State *states.State + Schemas *terraform.Schemas + Want string }{ { - &format.StateOpts{ - State: &states.State{}, - Color: color, - Schemas: &terraform.Schemas{}, - }, - "The state file is empty. No resources are represented.\n", + State: &states.State{}, + Schemas: &terraform.Schemas{}, + Want: "The state file is empty. No resources are represented.\n", }, { - &format.StateOpts{ - State: basicState(t), - Color: color, - Schemas: testSchemas(), - }, - basicStateOutput, + State: basicState(t), + Schemas: testSchemas(), + Want: basicStateOutput, }, { - &format.StateOpts{ - State: nestedState(t), - Color: color, - Schemas: testSchemas(), - }, - nestedStateOutput, + State: nestedState(t), + Schemas: testSchemas(), + Want: nestedStateOutput, }, { - &format.StateOpts{ - State: deposedState(t), - Color: color, - Schemas: testSchemas(), - }, - deposedNestedStateOutput, + State: deposedState(t), + Schemas: testSchemas(), + Want: deposedNestedStateOutput, }, { - &format.StateOpts{ - State: onlyDeposedState(t), - Color: color, - Schemas: testSchemas(), - }, - onlyDeposedOutput, + State: onlyDeposedState(t), + Schemas: testSchemas(), + Want: onlyDeposedOutput, }, { - &format.StateOpts{ - State: stateWithMoreOutputs(t), - Color: color, - Schemas: testSchemas(), - }, - stateWithMoreOutputsOutput, + State: stateWithMoreOutputs(t), + Schemas: testSchemas(), + Want: stateWithMoreOutputsOutput, }, } @@ -83,8 +65,8 @@ func TestState(t *testing.T) { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { root, outputs, err := jsonstate.MarshalForRenderer(&statefile.File{ - State: tt.State.State, - }, tt.State.Schemas) + State: tt.State, + }, tt.Schemas) if err != nil { t.Errorf("found err: %v", err) @@ -102,7 +84,7 @@ func TestState(t *testing.T) { RootModule: root, RootModuleOutputs: outputs, ProviderFormatVersion: jsonprovider.FormatVersion, - ProviderSchemas: jsonprovider.MarshalForRenderer(tt.State.Schemas), + ProviderSchemas: jsonprovider.MarshalForRenderer(tt.Schemas), }) result := done(t).All() diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 3abd63c489..5c8a78ca70 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -8,8 +8,11 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/command/arguments" - "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" "github.com/mitchellh/cli" ) @@ -24,19 +27,19 @@ func (c *StateShowCommand) Run(args []string) int { cmdFlags := c.Meta.defaultFlagSet("state show") cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + c.Streams.Eprintf("Error parsing command-line flags: %s\n", err.Error()) return 1 } args = cmdFlags.Args() if len(args) != 1 { - c.Ui.Error("Exactly one argument expected.\n") + c.Streams.Eprint("Exactly one argument expected.\n") return cli.RunResultHelp } // Check for user-supplied plugin path var err error if c.pluginPath, err = c.loadPluginPath(); err != nil { - c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) + c.Streams.Eprintf("Error loading plugin path: %\n", err) return 1 } @@ -50,7 +53,7 @@ func (c *StateShowCommand) Run(args []string) int { // We require a local backend local, ok := b.(backend.Local) if !ok { - c.Ui.Error(ErrUnsupportedLocalOp) + c.Streams.Eprint(ErrUnsupportedLocalOp) return 1 } @@ -60,14 +63,14 @@ func (c *StateShowCommand) Run(args []string) int { // Check if the address can be parsed addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) if addrDiags.HasErrors() { - c.Ui.Error(fmt.Sprintf(errParsingAddress, args[0])) + c.Streams.Eprintln(fmt.Sprintf(errParsingAddress, args[0])) return 1 } // We expect the config dir to always be the cwd cwd, err := os.Getwd() if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting cwd: %s", err)) + c.Streams.Eprintf("Error getting cwd: %s\n", err) return 1 } @@ -78,49 +81,49 @@ func (c *StateShowCommand) Run(args []string) int { opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing config loader: %s", err)) + c.Streams.Eprintf("Error initializing config loader: %s\n", err) return 1 } // Get the context (required to get the schemas) lr, _, ctxDiags := local.LocalRun(opReq) if ctxDiags.HasErrors() { - c.showDiagnostics(ctxDiags) + c.View.Diagnostics(ctxDiags) return 1 } // Get the schemas from the context schemas, diags := lr.Core.Schemas(lr.Config, lr.InputState) if diags.HasErrors() { - c.showDiagnostics(diags) + c.View.Diagnostics(diags) return 1 } // Get the state env, err := c.Workspace() if err != nil { - c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) + c.Streams.Eprintf("Error selecting workspace: %s\n", err) return 1 } stateMgr, err := b.StateMgr(env) if err != nil { - c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) + c.Streams.Eprintln(fmt.Sprintf(errStateLoadingState, err)) return 1 } if err := stateMgr.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to refresh state: %s", err)) + c.Streams.Eprintf("Failed to refresh state: %s\n", err) return 1 } state := stateMgr.State() if state == nil { - c.Ui.Error(errStateNotFound) + c.Streams.Eprintln(errStateNotFound) return 1 } is := state.ResourceInstance(addr) if !is.HasCurrent() { - c.Ui.Error(errNoInstanceFound) + c.Streams.Eprintln(errNoInstanceFound) return 1 } @@ -138,13 +141,26 @@ func (c *StateShowCommand) Run(args []string) int { absPc, ) - output := format.State(&format.StateOpts{ - State: singleInstance, - Color: c.Colorize(), - Schemas: schemas, - }) - c.Ui.Output(output[strings.Index(output, "#"):]) + root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(singleInstance, "", 0), schemas) + if err != nil { + c.Streams.Eprintf("Failed to marshal state to json: %s", err) + } + + jstate := jsonformat.State{ + StateFormatVersion: jsonstate.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + RootModule: root, + RootModuleOutputs: outputs, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + } + + renderer := jsonformat.Renderer{ + Streams: c.Streams, + Colorize: c.Colorize(), + RunningInAutomation: c.RunningInAutomation, + } + renderer.RenderHumanState(jstate) return 0 } diff --git a/internal/command/state_show_test.go b/internal/command/state_show_test.go index 3da87c0eca..f288e9b146 100644 --- a/internal/command/state_show_test.go +++ b/internal/command/state_show_test.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" - "github.com/mitchellh/cli" + "github.com/hashicorp/terraform/internal/terminal" "github.com/zclconf/go-cty/cty" ) @@ -47,11 +47,11 @@ func TestStateShow(t *testing.T) { }, } - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -59,13 +59,15 @@ func TestStateShow(t *testing.T) { "-state", statePath, "test_instance.foo", } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Test that outputs were displayed expected := strings.TrimSpace(testStateShowOutput) + "\n" - actual := ui.OutputWriter.String() + actual := output.Stdout() if actual != expected { t.Fatalf("Expected:\n%q\n\nTo equal:\n%q", actual, expected) } @@ -122,11 +124,11 @@ func TestStateShow_multi(t *testing.T) { }, } - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -134,13 +136,15 @@ func TestStateShow_multi(t *testing.T) { "-state", statePath, "test_instance.foo", } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Test that outputs were displayed expected := strings.TrimSpace(testStateShowOutput) + "\n" - actual := ui.OutputWriter.String() + actual := output.Stdout() if actual != expected { t.Fatalf("Expected:\n%q\n\nTo equal:\n%q", actual, expected) } @@ -150,11 +154,11 @@ func TestStateShow_noState(t *testing.T) { testCwd(t) p := testProvider() - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -164,8 +168,9 @@ func TestStateShow_noState(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("bad: %d", code) } - if !strings.Contains(ui.ErrorWriter.String(), "No state file was found!") { - t.Fatalf("expected a no state file error, got: %s", ui.ErrorWriter.String()) + output := done(t) + if !strings.Contains(output.Stderr(), "No state file was found!") { + t.Fatalf("expected a no state file error, got: %s", output.Stderr()) } } @@ -174,11 +179,11 @@ func TestStateShow_emptyState(t *testing.T) { statePath := testStateFile(t, state) p := testProvider() - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -189,8 +194,9 @@ func TestStateShow_emptyState(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("bad: %d", code) } - if !strings.Contains(ui.ErrorWriter.String(), "No instance found for the given address!") { - t.Fatalf("expected a no instance found error, got: %s", ui.ErrorWriter.String()) + output := done(t) + if !strings.Contains(output.Stderr(), "No instance found for the given address!") { + t.Fatalf("expected a no instance found error, got: %s", output.Stderr()) } } @@ -229,7 +235,7 @@ func TestStateShow_configured_provider(t *testing.T) { }, } - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: &testingOverrides{ @@ -237,7 +243,7 @@ func TestStateShow_configured_provider(t *testing.T) { addrs.NewDefaultProvider("test-beta"): providers.FactoryFixed(p), }, }, - Ui: ui, + Streams: streams, }, } @@ -245,13 +251,15 @@ func TestStateShow_configured_provider(t *testing.T) { "-state", statePath, "test_instance.foo", } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Test that outputs were displayed expected := strings.TrimSpace(testStateShowOutput) + "\n" - actual := ui.OutputWriter.String() + actual := output.Stdout() if actual != expected { t.Fatalf("Expected:\n%q\n\nTo equal:\n%q", actual, expected) }