diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 9b6c7e3a44..608c444117 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -96,28 +96,16 @@ func (b *Local) opApply( return } - trivialPlan := plan.Diff == nil || plan.Diff.Empty() + dispPlan := format.NewPlan(plan) + trivialPlan := dispPlan.Empty() hasUI := op.UIOut != nil && op.UIIn != nil - if hasUI && ((op.Destroy && !op.DestroyForce) || - (!op.Destroy && !op.AutoApprove && !trivialPlan)) { + mustConfirm := hasUI && ((op.Destroy && !op.DestroyForce) || (!op.Destroy && !op.AutoApprove && !trivialPlan)) + if mustConfirm { var desc, query string if op.Destroy { // Default destroy message - desc = "Terraform will delete all your managed infrastructure, as shown above.\n" + + desc = "Terraform will destroy all your managed infrastructure, as shown above.\n" + "There is no undo. Only 'yes' will be accepted to confirm." - - // If targets are specified, list those to user - if op.Targets != nil { - var descBuffer bytes.Buffer - descBuffer.WriteString("Terraform will delete the following infrastructure:\n") - for _, target := range op.Targets { - descBuffer.WriteString("\t") - descBuffer.WriteString(target) - descBuffer.WriteString("\n") - } - descBuffer.WriteString("There is no undo. Only 'yes' will be accepted to confirm") - desc = descBuffer.String() - } query = "Do you really want to destroy?" } else { desc = "Terraform will apply the changes described above.\n" + @@ -132,11 +120,7 @@ func (b *Local) opApply( } else { op.UIOut.Output("\n" + strings.TrimSpace(approvePlanHeader) + "\n") } - op.UIOut.Output(format.Plan(&format.PlanOpts{ - Plan: plan, - Color: b.Colorize(), - ModuleDepth: -1, - })) + op.UIOut.Output(dispPlan.Format(b.Colorize())) } v, err := op.UIIn.Input(&terraform.InputOpts{ diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 42a56eb291..83d1733dec 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -133,7 +133,8 @@ func (b *Local) opPlan( // Perform some output tasks if we have a CLI to output to. if b.CLI != nil { - if plan.Diff.Empty() { + dispPlan := format.NewPlan(plan) + if dispPlan.Empty() { b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planNoChanges))) return } @@ -146,18 +147,14 @@ func (b *Local) opPlan( path)) } - b.CLI.Output(format.Plan(&format.PlanOpts{ - Plan: plan, - Color: b.Colorize(), - ModuleDepth: -1, - })) + b.CLI.Output(dispPlan.Format(b.Colorize())) + stats := dispPlan.Stats() b.CLI.Output(b.Colorize().Color(fmt.Sprintf( "[reset][bold]Plan:[reset] "+ "%d to add, %d to change, %d to destroy.", - countHook.ToAdd+countHook.ToRemoveAndAdd, - countHook.ToChange, - countHook.ToRemove+countHook.ToRemoveAndAdd))) + stats.ToAdd, stats.ToChange, stats.ToDestroy, + ))) } } diff --git a/command/format/plan.go b/command/format/plan.go index 6bc699e398..1220b85a5e 100644 --- a/command/format/plan.go +++ b/command/format/plan.go @@ -11,232 +11,315 @@ import ( "github.com/mitchellh/colorstring" ) -// PlanOpts are the options for formatting a plan. -type PlanOpts struct { - // Plan is the plan to format. This is required. - Plan *terraform.Plan - - // Color is the colorizer. This is optional. - Color *colorstring.Colorize - - // ModuleDepth is the depth of the modules to expand. By default this - // is zero which will not expand modules at all. - ModuleDepth int +// Plan is a representation of a plan optimized for display to +// an end-user, as opposed to terraform.Plan which is for internal use. +// +// DisplayPlan excludes implementation details that may otherwise appear +// in the main plan, such as destroy actions on data sources (which are +// there only to clean up the state). +type Plan struct { + Resources []*InstanceDiff } -// Plan takes a plan and returns a -func Plan(opts *PlanOpts) string { - p := opts.Plan - if p.Diff == nil || p.Diff.Empty() { +// InstanceDiff is a representation of an instance diff optimized +// for display, in conjunction with DisplayPlan. +type InstanceDiff struct { + Addr *terraform.ResourceAddress + Action terraform.DiffChangeType + + // Attributes describes changes to the attributes of the instance. + // + // For destroy diffs this is always nil. + Attributes []*AttributeDiff + + Tainted bool + Deposed bool +} + +// AttributeDiff is a representation of an attribute diff optimized +// for display, in conjunction with DisplayInstanceDiff. +type AttributeDiff struct { + // Path is a dot-delimited traversal through possibly many levels of list and map structure, + // intended for display purposes only. + Path string + + Action terraform.DiffChangeType + + OldValue string + NewValue string + + NewComputed bool + Sensitive bool + ForcesNew bool +} + +// PlanStats gives summary counts for a Plan. +type PlanStats struct { + ToAdd, ToChange, ToDestroy int +} + +// NewPlan produces a display-oriented Plan from a terraform.Plan. +func NewPlan(plan *terraform.Plan) *Plan { + ret := &Plan{} + if plan == nil || plan.Diff == nil || plan.Diff.Empty() { + // Nothing to do! + return ret + } + + for _, m := range plan.Diff.Modules { + var modulePath []string + if !m.IsRoot() { + // trim off the leading "root" path segment, since it's implied + // when we use a path in a resource address. + modulePath = m.Path[1:] + } + + for k, r := range m.Resources { + if r.Empty() { + continue + } + + addr, err := terraform.ParseResourceAddressForInstanceDiff(modulePath, k) + if err != nil { + // should never happen; indicates invalid diff + panic("invalid resource address in diff") + } + + dataSource := addr.Mode == config.DataResourceMode + + // We create "destroy" actions for data resources so we can clean + // up their entries in state, but this is an implementation detail + // that users shouldn't see. + if dataSource && r.ChangeType() == terraform.DiffDestroy { + continue + } + + did := &InstanceDiff{ + Addr: addr, + Action: r.ChangeType(), + Tainted: r.DestroyTainted, + Deposed: r.DestroyDeposed, + } + + if dataSource && did.Action == terraform.DiffCreate { + // Use "refresh" as the action for display, since core + // currently uses Create for this. + did.Action = terraform.DiffRefresh + } + + ret.Resources = append(ret.Resources, did) + + if did.Action == terraform.DiffDestroy { + // Don't show any outputs for destroy actions + continue + } + + for k, a := range r.Attributes { + var action terraform.DiffChangeType + switch { + case a.NewRemoved: + action = terraform.DiffDestroy + case did.Action == terraform.DiffCreate: + action = terraform.DiffCreate + default: + action = terraform.DiffUpdate + } + + did.Attributes = append(did.Attributes, &AttributeDiff{ + Path: k, + Action: action, + + OldValue: a.Old, + NewValue: a.New, + + Sensitive: a.Sensitive, + ForcesNew: a.RequiresNew, + NewComputed: a.NewComputed, + }) + } + + // Sort the attributes by their paths for display + sort.Slice(did.Attributes, func(i, j int) bool { + iPath := did.Attributes[i].Path + jPath := did.Attributes[j].Path + + // as a special case, "id" is always first + switch { + case iPath != jPath && (iPath == "id" || jPath == "id"): + return iPath == "id" + default: + return iPath < jPath + } + }) + + } + } + + // Sort the instance diffs by their addresses for display. + sort.Slice(ret.Resources, func(i, j int) bool { + iAddr := ret.Resources[i].Addr + jAddr := ret.Resources[j].Addr + return iAddr.Less(jAddr) + }) + + return ret +} + +// Format produces and returns a text representation of the receiving plan +// intended for display in a terminal. +// +// If color is not nil, it is used to colorize the output. +func (p *Plan) Format(color *colorstring.Colorize) string { + if p.Empty() { return "This plan does nothing." } - if opts.Color == nil { - opts.Color = &colorstring.Colorize{ + if color == nil { + color = &colorstring.Colorize{ Colors: colorstring.DefaultColors, Reset: false, } } - buf := new(bytes.Buffer) - for _, m := range p.Diff.Modules { - if len(m.Path)-1 <= opts.ModuleDepth || opts.ModuleDepth == -1 { - formatPlanModuleExpand(buf, m, opts) - } else { - formatPlanModuleSingle(buf, m, opts) + // Find the longest path length of all the paths that are changing, + // so we can align them all. + keyLen := 0 + for _, r := range p.Resources { + for _, attr := range r.Attributes { + key := attr.Path + + if len(key) > keyLen { + keyLen = len(key) + } } } + buf := new(bytes.Buffer) + for _, r := range p.Resources { + formatPlanInstanceDiff(buf, r, keyLen, color) + } + return strings.TrimSpace(buf.String()) } -// formatPlanModuleExpand will output the given module and all of its -// resources. -func formatPlanModuleExpand( - buf *bytes.Buffer, m *terraform.ModuleDiff, opts *PlanOpts) { - // Ignore empty diffs - if m.Empty() { - return - } - - var modulePath []string - if !m.IsRoot() { - modulePath = m.Path[1:] - } - - // We want to output the resources in sorted order to make things - // easier to scan through, so get all the resource names and sort them. - names := make([]string, 0, len(m.Resources)) - addrs := map[string]*terraform.ResourceAddress{} - for name := range m.Resources { - names = append(names, name) - var err error - addrs[name], err = terraform.ParseResourceAddressForInstanceDiff(modulePath, name) - if err != nil { - // should never happen; indicates invalid diff - panic("invalid resource address in diff") - } - } - sort.Slice(names, func(i, j int) bool { - return addrs[names[i]].Less(addrs[names[j]]) - }) - - // Go through each sorted name and start building the output - for _, name := range names { - rdiff := m.Resources[name] - if rdiff.Empty() { - continue - } - - addr := addrs[name] - addrStr := addr.String() - dataSource := addr.Mode == config.DataResourceMode - - // Determine the color for the text (green for adding, yellow - // for change, red for delete), and symbol, and output the - // resource header. - color := "yellow" - symbol := " ~" - oldValues := true - switch rdiff.ChangeType() { - case terraform.DiffDestroyCreate: - color = "yellow" - symbol = "[red]-[reset]/[green]+[reset][yellow]" +// Stats returns statistics about the plan +func (p *Plan) Stats() PlanStats { + var ret PlanStats + for _, r := range p.Resources { + switch r.Action { case terraform.DiffCreate: - color = "green" - symbol = " +" - oldValues = false - - // If we're "creating" a data resource then we'll present it - // to the user as a "read" operation, so it's clear that this - // operation won't change anything outside of the Terraform state. - // Unfortunately by the time we get here we only have the name - // to work with, so we need to cheat and exploit knowledge of the - // naming scheme for data resources. - if dataSource { - symbol = " <=" - color = "cyan" - } + ret.ToAdd++ + case terraform.DiffUpdate: + ret.ToChange++ + case terraform.DiffDestroyCreate: + ret.ToAdd++ + ret.ToDestroy++ case terraform.DiffDestroy: - color = "red" - symbol = " -" + ret.ToDestroy++ } - - var extraAttr []string - if rdiff.DestroyTainted { - extraAttr = append(extraAttr, "tainted") - } - if rdiff.DestroyDeposed { - extraAttr = append(extraAttr, "deposed") - } - var extraStr string - if len(extraAttr) > 0 { - extraStr = fmt.Sprintf(" (%s)", strings.Join(extraAttr, ", ")) - } - if rdiff.ChangeType() == terraform.DiffDestroyCreate { - extraStr = extraStr + opts.Color.Color(" [red][bold](new resource required)") - } - - buf.WriteString(opts.Color.Color(fmt.Sprintf( - "[%s]%s %s%s\n", - color, symbol, addrStr, extraStr))) - - // Get all the attributes that are changing, and sort them. Also - // determine the longest key so that we can align them all. - keyLen := 0 - keys := make([]string, 0, len(rdiff.Attributes)) - for key, _ := range rdiff.Attributes { - // Skip the ID since we do that specially - if key == "id" { - continue - } - - keys = append(keys, key) - if len(key) > keyLen { - keyLen = len(key) - } - } - sort.Strings(keys) - - // Go through and output each attribute - for _, attrK := range keys { - attrDiff := rdiff.Attributes[attrK] - - v := attrDiff.New - if v == "" && attrDiff.NewComputed { - v = "" - } - - if attrDiff.Sensitive { - v = "" - } - - updateMsg := "" - if attrDiff.RequiresNew && rdiff.Destroy { - updateMsg = opts.Color.Color(" [red](forces new resource)") - } else if attrDiff.Sensitive && oldValues { - updateMsg = opts.Color.Color(" [yellow](attribute changed)") - } - - if oldValues { - var u string - if attrDiff.Sensitive { - u = "" - } else { - u = attrDiff.Old - } - buf.WriteString(fmt.Sprintf( - " %s:%s %#v => %#v%s\n", - attrK, - strings.Repeat(" ", keyLen-len(attrK)), - u, - v, - updateMsg)) - } else { - buf.WriteString(fmt.Sprintf( - " %s:%s %#v%s\n", - attrK, - strings.Repeat(" ", keyLen-len(attrK)), - v, - updateMsg)) - } - } - - // Write the reset color so we don't overload the user's terminal - buf.WriteString(opts.Color.Color("[reset]\n")) } + return ret } -// formatPlanModuleSingle will output the given module and all of its -// resources. -func formatPlanModuleSingle( - buf *bytes.Buffer, m *terraform.ModuleDiff, opts *PlanOpts) { - // Ignore empty diffs - if m.Empty() { - return - } +// Empty returns true if there is at least one resource diff in the receiving plan. +func (p *Plan) Empty() bool { + return len(p.Resources) == 0 +} - moduleName := fmt.Sprintf("module.%s", strings.Join(m.Path[1:], ".")) +// formatPlanInstanceDiff writes the text representation of the given instance diff +// to the given buffer, using the given colorizer. +func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colorizer *colorstring.Colorize) { + addrStr := r.Addr.String() // Determine the color for the text (green for adding, yellow // for change, red for delete), and symbol, and output the // resource header. color := "yellow" - symbol := "~" - switch m.ChangeType() { + symbol := " ~" + oldValues := true + switch r.Action { + case terraform.DiffDestroyCreate: + color = "yellow" + symbol = "[red]-[reset]/[green]+[reset][yellow]" case terraform.DiffCreate: color = "green" - symbol = "+" + symbol = " +" + oldValues = false case terraform.DiffDestroy: color = "red" - symbol = "-" + symbol = " -" + case terraform.DiffRefresh: + symbol = " <=" + color = "cyan" + oldValues = false } - buf.WriteString(opts.Color.Color(fmt.Sprintf( - "[%s]%s %s\n", - color, symbol, moduleName))) - buf.WriteString(fmt.Sprintf( - " %d resource(s)", - len(m.Resources))) - buf.WriteString(opts.Color.Color("[reset]\n")) + var extraStr string + if r.Tainted { + extraStr = extraStr + colorizer.Color(" (tainted)") + } + if r.Deposed { + extraStr = extraStr + colorizer.Color(" (deposed)") + } + if r.Action == terraform.DiffDestroyCreate { + extraStr = extraStr + colorizer.Color(" [red][bold](new resource required)") + } + + buf.WriteString( + colorizer.Color(fmt.Sprintf( + "[%s]%s %s%s\n", + color, symbol, addrStr, extraStr, + )), + ) + + for _, attr := range r.Attributes { + + v := attr.NewValue + var dispV string + switch { + case v == "" && attr.NewComputed: + dispV = "" + case attr.Sensitive: + dispV = "" + default: + dispV = fmt.Sprintf("%q", v) + } + + updateMsg := "" + switch { + case attr.ForcesNew && r.Action == terraform.DiffDestroy: + updateMsg = colorizer.Color(" [red](forces new resource)") + case attr.Sensitive && oldValues: + updateMsg = colorizer.Color(" [yellow](attribute changed)") + } + + if oldValues { + u := attr.OldValue + var dispU string + switch { + case attr.Sensitive: + dispU = "" + default: + dispU = fmt.Sprintf("%q", u) + } + buf.WriteString(fmt.Sprintf( + " %s:%s %s => %s%s\n", + attr.Path, + strings.Repeat(" ", keyLen-len(attr.Path)), + dispU, dispV, + updateMsg, + )) + } else { + buf.WriteString(fmt.Sprintf( + " %s:%s %s%s\n", + attr.Path, + strings.Repeat(" ", keyLen-len(attr.Path)), + dispV, + updateMsg, + )) + } + } + + // Write the reset color so we don't bleed color into later text + buf.WriteString(colorizer.Color("[reset]\n")) } diff --git a/command/format/plan_test.go b/command/format/plan_test.go index 82390d3d9d..7ee3802617 100644 --- a/command/format/plan_test.go +++ b/command/format/plan_test.go @@ -1,14 +1,452 @@ package format import ( + "reflect" "strings" "testing" + "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/colorstring" ) -// Test that a root level data source gets a special plan output on create +var disabledColorize = &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: true, +} + +func TestNewPlan(t *testing.T) { + tests := map[string]struct { + Input *terraform.Plan + Want *Plan + }{ + "nil input": { + Input: nil, + Want: &Plan{ + Resources: nil, + }, + }, + "nil diff": { + Input: &terraform.Plan{}, + Want: &Plan{ + Resources: nil, + }, + }, + "empty diff": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{}, + }, + }, + }, + }, + Want: &Plan{ + Resources: nil, + }, + }, + "create managed resource": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{ + "test_resource.foo": { + Attributes: map[string]*terraform.ResourceAttrDiff{ + "id": { + NewComputed: true, + RequiresNew: true, + }, + }, + }, + }, + }, + }, + }, + }, + Want: &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffCreate, + Attributes: []*AttributeDiff{ + { + Path: "id", + Action: terraform.DiffCreate, + NewComputed: true, + ForcesNew: true, + }, + }, + }, + }, + }, + }, + "create managed resource in child module": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{ + "test_resource.foo": { + Attributes: map[string]*terraform.ResourceAttrDiff{ + "id": { + NewComputed: true, + RequiresNew: true, + }, + }, + }, + }, + }, + { + Path: []string{"root", "foo"}, + Resources: map[string]*terraform.InstanceDiff{ + "test_resource.foo": { + Attributes: map[string]*terraform.ResourceAttrDiff{ + "id": { + NewComputed: true, + RequiresNew: true, + }, + }, + }, + }, + }, + }, + }, + }, + Want: &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffCreate, + Attributes: []*AttributeDiff{ + { + Path: "id", + Action: terraform.DiffCreate, + NewComputed: true, + ForcesNew: true, + }, + }, + }, + { + Addr: mustParseResourceAddress("module.foo.test_resource.foo"), + Action: terraform.DiffCreate, + Attributes: []*AttributeDiff{ + { + Path: "id", + Action: terraform.DiffCreate, + NewComputed: true, + ForcesNew: true, + }, + }, + }, + }, + }, + }, + "create data resource": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{ + "data.test_data_source.foo": { + Attributes: map[string]*terraform.ResourceAttrDiff{ + "id": { + NewComputed: true, + RequiresNew: true, + }, + }, + }, + }, + }, + }, + }, + }, + Want: &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("data.test_data_source.foo"), + Action: terraform.DiffRefresh, + Attributes: []*AttributeDiff{ + { + Path: "id", + Action: terraform.DiffUpdate, + NewComputed: true, + ForcesNew: true, + }, + }, + }, + }, + }, + }, + "destroy managed resource": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{ + "test_resource.foo": { + Destroy: true, + }, + }, + }, + }, + }, + }, + Want: &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffDestroy, + }, + }, + }, + }, + "destroy data resource": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{ + "data.test_data_source.foo": { + Destroy: true, + }, + }, + }, + }, + }, + }, + Want: &Plan{ + // Data source destroys are not shown + Resources: nil, + }, + }, + "destroy many instances of a resource": { + Input: &terraform.Plan{ + Diff: &terraform.Diff{ + Modules: []*terraform.ModuleDiff{ + { + Path: []string{"root"}, + Resources: map[string]*terraform.InstanceDiff{ + "test_resource.foo.0": { + Destroy: true, + }, + "test_resource.foo.1": { + Destroy: true, + }, + "test_resource.foo.10": { + Destroy: true, + }, + "test_resource.foo.2": { + Destroy: true, + }, + "test_resource.foo.3": { + Destroy: true, + }, + "test_resource.foo.4": { + Destroy: true, + }, + "test_resource.foo.5": { + Destroy: true, + }, + "test_resource.foo.6": { + Destroy: true, + }, + "test_resource.foo.7": { + Destroy: true, + }, + "test_resource.foo.8": { + Destroy: true, + }, + "test_resource.foo.9": { + Destroy: true, + }, + }, + }, + }, + }, + }, + Want: &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo[0]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[1]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[2]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[3]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[4]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[5]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[6]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[7]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[8]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[9]"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.foo[10]"), + Action: terraform.DiffDestroy, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := NewPlan(test.Input) + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %sgot: %swant:%s", + spew.Sdump(test.Input), + spew.Sdump(got), + spew.Sdump(test.Want), + ) + } + }) + } +} + +func TestPlanStats(t *testing.T) { + tests := map[string]struct { + Input *Plan + Want PlanStats + }{ + "empty": { + &Plan{}, + PlanStats{}, + }, + "destroy": { + &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffDestroy, + }, + { + Addr: mustParseResourceAddress("test_resource.bar"), + Action: terraform.DiffDestroy, + }, + }, + }, + PlanStats{ + ToDestroy: 2, + }, + }, + "create": { + &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffCreate, + }, + { + Addr: mustParseResourceAddress("test_resource.bar"), + Action: terraform.DiffCreate, + }, + }, + }, + PlanStats{ + ToAdd: 2, + }, + }, + "update": { + &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffUpdate, + }, + { + Addr: mustParseResourceAddress("test_resource.bar"), + Action: terraform.DiffUpdate, + }, + }, + }, + PlanStats{ + ToChange: 2, + }, + }, + "data source refresh": { + &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("data.test.foo"), + Action: terraform.DiffRefresh, + }, + }, + }, + PlanStats{ + // data resource refreshes are not counted in our stats + }, + }, + "replace": { + &Plan{ + Resources: []*InstanceDiff{ + { + Addr: mustParseResourceAddress("test_resource.foo"), + Action: terraform.DiffDestroyCreate, + }, + { + Addr: mustParseResourceAddress("test_resource.bar"), + Action: terraform.DiffDestroyCreate, + }, + }, + }, + PlanStats{ + ToDestroy: 2, + ToAdd: 2, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := test.Input.Stats() + if !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %sgot: %swant:%s", + spew.Sdump(test.Input), + spew.Sdump(got), + spew.Sdump(test.Want), + ) + } + }) + } +} + +// Test that deposed instances are marked as such func TestPlan_destroyDeposed(t *testing.T) { plan := &terraform.Plan{ Diff: &terraform.Diff{ @@ -24,16 +462,8 @@ func TestPlan_destroyDeposed(t *testing.T) { }, }, } - opts := &PlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 1, - } - - actual := Plan(opts) + dispPlan := NewPlan(plan) + actual := dispPlan.Format(disabledColorize) expected := strings.TrimSpace(` - aws_instance.foo (deposed) @@ -64,16 +494,8 @@ func TestPlan_displayInterpolations(t *testing.T) { }, }, } - opts := &PlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 1, - } - - out := Plan(opts) + dispPlan := NewPlan(plan) + out := dispPlan.Format(disabledColorize) lines := strings.Split(out, "\n") if len(lines) != 2 { t.Fatal("expected 2 lines of output, got:\n", out) @@ -108,16 +530,8 @@ func TestPlan_rootDataSource(t *testing.T) { }, }, } - opts := &PlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 1, - } - - actual := Plan(opts) + dispPlan := NewPlan(plan) + actual := dispPlan.Format(disabledColorize) expected := strings.TrimSpace(` <= data.type.name @@ -149,16 +563,8 @@ func TestPlan_nestedDataSource(t *testing.T) { }, }, } - opts := &PlanOpts{ - Plan: plan, - Color: &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - }, - ModuleDepth: 2, - } - - actual := Plan(opts) + dispPlan := NewPlan(plan) + actual := dispPlan.Format(disabledColorize) expected := strings.TrimSpace(` <= module.nested.data.type.name @@ -168,3 +574,11 @@ func TestPlan_nestedDataSource(t *testing.T) { t.Fatalf("expected:\n\n%s\n\ngot:\n\n%s", expected, actual) } } + +func mustParseResourceAddress(s string) *terraform.ResourceAddress { + addr, err := terraform.ParseResourceAddress(s) + if err != nil { + panic(err) + } + return addr +} diff --git a/command/show.go b/command/show.go index 254644863b..d8d1c84efe 100644 --- a/command/show.go +++ b/command/show.go @@ -110,11 +110,8 @@ func (c *ShowCommand) Run(args []string) int { } if plan != nil { - c.Ui.Output(format.Plan(&format.PlanOpts{ - Plan: plan, - Color: c.Colorize(), - ModuleDepth: moduleDepth, - })) + dispPlan := format.NewPlan(plan) + c.Ui.Output(dispPlan.Format(c.Colorize())) return 0 } diff --git a/terraform/diff.go b/terraform/diff.go index fd1687e7ed..9cbd5d88fa 100644 --- a/terraform/diff.go +++ b/terraform/diff.go @@ -23,6 +23,12 @@ const ( DiffUpdate DiffDestroy DiffDestroyCreate + + // DiffRefresh is only used in the UI for displaying diffs. + // Managed resource reads never appear in plan, and when data source + // reads appear they are represented as DiffCreate in core before + // transforming to DiffRefresh in the UI layer. + DiffRefresh // TODO: Actually use DiffRefresh in core too, for less confusion ) // multiVal matches the index key to a flatmapped set, list or map