From 83414beb8f251acd24a3eca504ac43d2a52f614d Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 31 Aug 2017 19:19:06 -0700 Subject: [PATCH] command: various adjustments to the diff presentation The previous diff presentation was rather "wordy", and not very friendly to those who can't see color either because they have color-blindness or because they don't have a color-supporting terminal. This new presentation uses the actual symbols used in the plan output and tries to be more concise. It also uses some framing characters to try to separate the different stages of "terraform plan" to make it easier to visually navigate. The apply command also adopts this new plan presentation, in preparation for "terraform apply" (with interactive plan confirmation) becoming the primary, safe workflow in the next major release. Finally, we standardize on the terminology "perform" and "actions" rather than "execute" and "changes" to reflect the fact that reading is now an action and that isn't actually a _change_. --- backend/local/backend_apply.go | 26 ++-------- backend/local/backend_plan.go | 92 ++++++++++++++++++++++------------ command/format/plan.go | 42 ++++++++++++---- 3 files changed, 98 insertions(+), 62 deletions(-) diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 608c44411750..4463431815d3 100644 --- a/backend/local/backend_apply.go +++ b/backend/local/backend_apply.go @@ -108,19 +108,15 @@ func (b *Local) opApply( "There is no undo. Only 'yes' will be accepted to confirm." query = "Do you really want to destroy?" } else { - desc = "Terraform will apply the changes described above.\n" + + desc = "Terraform will perform the actions described above.\n" + "Only 'yes' will be accepted to approve." - query = "Do you want to apply these changes?" + query = "Do you want to perform these actions?" } if !trivialPlan { // Display the plan of what we are going to apply/destroy. - if op.Destroy { - op.UIOut.Output("\n" + strings.TrimSpace(approveDestroyPlanHeader) + "\n") - } else { - op.UIOut.Output("\n" + strings.TrimSpace(approvePlanHeader) + "\n") - } - op.UIOut.Output(dispPlan.Format(b.Colorize())) + b.renderPlan(dispPlan) + b.CLI.Output("") } v, err := op.UIIn.Input(&terraform.InputOpts{ @@ -337,17 +333,3 @@ Terraform encountered an error attempting to save the state before canceling the current operation. Once the operation is complete another attempt will be made to save the final state. ` - -const approvePlanHeader = ` -The Terraform execution plan has been generated and is shown below. -Resources are shown in alphabetical order for quick scanning. Green resources -will be created (or destroyed and then created if an existing resource -exists), yellow resources are being changed in-place, and red resources -will be destroyed. Cyan entries are data sources to be read. -` - -const approveDestroyPlanHeader = ` -The Terraform destroy plan has been generated and is shown below. -Resources are shown in alphabetical order for quick scanning. -Resources shown in red will be destroyed. -` diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 83d1733dec76..905f89f1a917 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -1,6 +1,7 @@ package local import ( + "bytes" "context" "fmt" "log" @@ -95,6 +96,9 @@ func (b *Local) opPlan( runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) return } + if b.CLI != nil { + b.CLI.Output("\n------------------------------------------------------------------------") + } } // Perform the plan @@ -135,27 +139,60 @@ func (b *Local) opPlan( if b.CLI != nil { dispPlan := format.NewPlan(plan) if dispPlan.Empty() { - b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planNoChanges))) + b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges))) return } + b.renderPlan(dispPlan) + + b.CLI.Output("\n------------------------------------------------------------------------") + if path := op.PlanOutPath; path == "" { - b.CLI.Output(strings.TrimSpace(planHeaderNoOutput) + "\n") + b.CLI.Output(fmt.Sprintf( + "\n" + strings.TrimSpace(planHeaderNoOutput) + "\n", + )) } else { b.CLI.Output(fmt.Sprintf( - strings.TrimSpace(planHeaderYesOutput)+"\n", - path)) + "\n"+strings.TrimSpace(planHeaderYesOutput)+"\n", + path, path, + )) } + } +} - b.CLI.Output(dispPlan.Format(b.Colorize())) +func (b *Local) renderPlan(dispPlan *format.Plan) { - stats := dispPlan.Stats() - b.CLI.Output(b.Colorize().Color(fmt.Sprintf( - "[reset][bold]Plan:[reset] "+ - "%d to add, %d to change, %d to destroy.", - stats.ToAdd, stats.ToChange, stats.ToDestroy, - ))) + headerBuf := &bytes.Buffer{} + fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro)) + counts := dispPlan.ActionCounts() + if counts[terraform.DiffCreate] > 0 { + fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(terraform.DiffCreate)) + } + if counts[terraform.DiffUpdate] > 0 { + fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(terraform.DiffUpdate)) + } + if counts[terraform.DiffDestroy] > 0 { + fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(terraform.DiffDestroy)) } + if counts[terraform.DiffDestroyCreate] > 0 { + fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(terraform.DiffDestroyCreate)) + } + if counts[terraform.DiffRefresh] > 0 { + fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(terraform.DiffRefresh)) + } + + b.CLI.Output(b.Colorize().Color(headerBuf.String())) + + b.CLI.Output("Terraform will perform the following actions:\n") + + 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.", + stats.ToAdd, stats.ToChange, stats.ToDestroy, + ))) } const planErrNoConfig = ` @@ -168,37 +205,30 @@ flag or create a single empty configuration file. Otherwise, please create a Terraform configuration file in the path being executed and try again. ` +const planHeaderIntro = ` +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: +` + const planHeaderNoOutput = ` -The Terraform execution plan has been generated and is shown below. -Resources are shown in alphabetical order for quick scanning. Green resources -will be created (or destroyed and then created if an existing resource -exists), yellow resources are being changed in-place, and red resources -will be destroyed. Cyan entries are data sources to be read. - -Note: You didn't specify an "-out" parameter to save this plan, so when -"apply" is called, Terraform can't guarantee this is what will execute. +Note: You didn't specify an "-out" parameter to save this plan, so Terraform +can't guarantee that exactly these actions will be performed if +"terraform apply" is subsequently run. ` const planHeaderYesOutput = ` -The Terraform execution plan has been generated and is shown below. -Resources are shown in alphabetical order for quick scanning. Green resources -will be created (or destroyed and then created if an existing resource -exists), yellow resources are being changed in-place, and red resources -will be destroyed. Cyan entries are data sources to be read. - -Your plan was also saved to the path below. Call the "apply" subcommand -with this plan file and Terraform will exactly execute this execution -plan. +This plan was saved to: %s -Path: %s +To perform exactly these actions, run the following command to apply: + terraform apply %q ` const planNoChanges = ` [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green] This means that Terraform did not detect any differences between your -configuration and real physical resources that exist. As a result, Terraform -doesn't need to do anything. +configuration and real physical resources that exist. As a result, no +actions need to be performed. ` const planRefreshing = ` diff --git a/command/format/plan.go b/command/format/plan.go index 1220b85a5e7d..728345e0a8b9 100644 --- a/command/format/plan.go +++ b/command/format/plan.go @@ -221,11 +221,39 @@ func (p *Plan) Stats() PlanStats { return ret } +// ActionCounts returns the number of diffs for each action type +func (p *Plan) ActionCounts() map[terraform.DiffChangeType]int { + ret := map[terraform.DiffChangeType]int{} + for _, r := range p.Resources { + ret[r.Action]++ + } + return ret +} + // 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 } +// 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 terraform.DiffChangeType) string { + switch action { + case terraform.DiffDestroyCreate: + return "[red]-[reset]/[green]+[reset]" + case terraform.DiffCreate: + return " [green]+[reset]" + case terraform.DiffDestroy: + return " [red]-[reset]" + case terraform.DiffRefresh: + return " [cyan]<=[reset]" + default: + return " [yellow]~[reset]" + } +} + // 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) { @@ -235,31 +263,27 @@ func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colo // for change, red for delete), and symbol, and output the // resource header. color := "yellow" - symbol := " ~" + symbol := DiffActionSymbol(r.Action) oldValues := true switch r.Action { case terraform.DiffDestroyCreate: color = "yellow" - symbol = "[red]-[reset]/[green]+[reset][yellow]" case terraform.DiffCreate: color = "green" - symbol = " +" oldValues = false case terraform.DiffDestroy: color = "red" - symbol = " -" case terraform.DiffRefresh: - symbol = " <=" color = "cyan" oldValues = false } var extraStr string if r.Tainted { - extraStr = extraStr + colorizer.Color(" (tainted)") + extraStr = extraStr + " (tainted)" } if r.Deposed { - extraStr = extraStr + colorizer.Color(" (deposed)") + extraStr = extraStr + " (deposed)" } if r.Action == terraform.DiffDestroyCreate { extraStr = extraStr + colorizer.Color(" [red][bold](new resource required)") @@ -267,8 +291,8 @@ func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colo buf.WriteString( colorizer.Color(fmt.Sprintf( - "[%s]%s %s%s\n", - color, symbol, addrStr, extraStr, + "[%s]%s [%s]%s%s\n", + color, symbol, color, addrStr, extraStr, )), )