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_.
This commit is contained in:
Martin Atkins 2017-08-31 19:19:06 -07:00
parent 892f60efe0
commit 83414beb8f
3 changed files with 100 additions and 64 deletions

View File

@ -108,19 +108,15 @@ func (b *Local) opApply(
"There is no undo. Only 'yes' will be accepted to confirm." "There is no undo. Only 'yes' will be accepted to confirm."
query = "Do you really want to destroy?" query = "Do you really want to destroy?"
} else { } 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." "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 { if !trivialPlan {
// Display the plan of what we are going to apply/destroy. // Display the plan of what we are going to apply/destroy.
if op.Destroy { b.renderPlan(dispPlan)
op.UIOut.Output("\n" + strings.TrimSpace(approveDestroyPlanHeader) + "\n") b.CLI.Output("")
} else {
op.UIOut.Output("\n" + strings.TrimSpace(approvePlanHeader) + "\n")
}
op.UIOut.Output(dispPlan.Format(b.Colorize()))
} }
v, err := op.UIIn.Input(&terraform.InputOpts{ 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 the current operation. Once the operation is complete another attempt will be
made to save the final state. 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.
`

View File

@ -1,6 +1,7 @@
package local package local
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"log" "log"
@ -95,6 +96,9 @@ func (b *Local) opPlan(
runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err) runningOp.Err = errwrap.Wrapf("Error refreshing state: {{err}}", err)
return return
} }
if b.CLI != nil {
b.CLI.Output("\n------------------------------------------------------------------------")
}
} }
// Perform the plan // Perform the plan
@ -135,17 +139,51 @@ func (b *Local) opPlan(
if b.CLI != nil { if b.CLI != nil {
dispPlan := format.NewPlan(plan) dispPlan := format.NewPlan(plan)
if dispPlan.Empty() { if dispPlan.Empty() {
b.CLI.Output(b.Colorize().Color(strings.TrimSpace(planNoChanges))) b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges)))
return return
} }
b.renderPlan(dispPlan)
b.CLI.Output("\n------------------------------------------------------------------------")
if path := op.PlanOutPath; path == "" { if path := op.PlanOutPath; path == "" {
b.CLI.Output(strings.TrimSpace(planHeaderNoOutput) + "\n") b.CLI.Output(fmt.Sprintf(
"\n" + strings.TrimSpace(planHeaderNoOutput) + "\n",
))
} else { } else {
b.CLI.Output(fmt.Sprintf( b.CLI.Output(fmt.Sprintf(
strings.TrimSpace(planHeaderYesOutput)+"\n", "\n"+strings.TrimSpace(planHeaderYesOutput)+"\n",
path)) path, path,
))
} }
}
}
func (b *Local) renderPlan(dispPlan *format.Plan) {
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())) b.CLI.Output(dispPlan.Format(b.Colorize()))
@ -155,7 +193,6 @@ func (b *Local) opPlan(
"%d to add, %d to change, %d to destroy.", "%d to add, %d to change, %d to destroy.",
stats.ToAdd, stats.ToChange, stats.ToDestroy, stats.ToAdd, stats.ToChange, stats.ToDestroy,
))) )))
}
} }
const planErrNoConfig = ` 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. a Terraform configuration file in the path being executed and try again.
` `
const planHeaderNoOutput = ` const planHeaderIntro = `
The Terraform execution plan has been generated and is shown below. An execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources Resource actions are indicated with the following symbols:
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 const planHeaderNoOutput = `
"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 = ` const planHeaderYesOutput = `
The Terraform execution plan has been generated and is shown below. This plan was saved to: %s
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 To perform exactly these actions, run the following command to apply:
with this plan file and Terraform will exactly execute this execution terraform apply %q
plan.
Path: %s
` `
const planNoChanges = ` const planNoChanges = `
[reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green] [reset][bold][green]No changes. Infrastructure is up-to-date.[reset][green]
This means that Terraform did not detect any differences between your This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, Terraform configuration and real physical resources that exist. As a result, no
doesn't need to do anything. actions need to be performed.
` `
const planRefreshing = ` const planRefreshing = `

View File

@ -221,11 +221,39 @@ func (p *Plan) Stats() PlanStats {
return ret 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. // Empty returns true if there is at least one resource diff in the receiving plan.
func (p *Plan) Empty() bool { func (p *Plan) Empty() bool {
return len(p.Resources) == 0 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 // formatPlanInstanceDiff writes the text representation of the given instance diff
// to the given buffer, using the given colorizer. // to the given buffer, using the given colorizer.
func formatPlanInstanceDiff(buf *bytes.Buffer, r *InstanceDiff, keyLen int, colorizer *colorstring.Colorize) { 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 // for change, red for delete), and symbol, and output the
// resource header. // resource header.
color := "yellow" color := "yellow"
symbol := " ~" symbol := DiffActionSymbol(r.Action)
oldValues := true oldValues := true
switch r.Action { switch r.Action {
case terraform.DiffDestroyCreate: case terraform.DiffDestroyCreate:
color = "yellow" color = "yellow"
symbol = "[red]-[reset]/[green]+[reset][yellow]"
case terraform.DiffCreate: case terraform.DiffCreate:
color = "green" color = "green"
symbol = " +"
oldValues = false oldValues = false
case terraform.DiffDestroy: case terraform.DiffDestroy:
color = "red" color = "red"
symbol = " -"
case terraform.DiffRefresh: case terraform.DiffRefresh:
symbol = " <="
color = "cyan" color = "cyan"
oldValues = false oldValues = false
} }
var extraStr string var extraStr string
if r.Tainted { if r.Tainted {
extraStr = extraStr + colorizer.Color(" (tainted)") extraStr = extraStr + " (tainted)"
} }
if r.Deposed { if r.Deposed {
extraStr = extraStr + colorizer.Color(" (deposed)") extraStr = extraStr + " (deposed)"
} }
if r.Action == terraform.DiffDestroyCreate { if r.Action == terraform.DiffDestroyCreate {
extraStr = extraStr + colorizer.Color(" [red][bold](new resource required)") 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( buf.WriteString(
colorizer.Color(fmt.Sprintf( colorizer.Color(fmt.Sprintf(
"[%s]%s %s%s\n", "[%s]%s [%s]%s%s\n",
color, symbol, addrStr, extraStr, color, symbol, color, addrStr, extraStr,
)), )),
) )