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."
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.
`

View File

@ -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,29 +139,62 @@ 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()))
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,
)))
}
}
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()))
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 = `
No configuration files found!
@ -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 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.
const planHeaderIntro = `
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
`
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.
const planHeaderNoOutput = `
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.
This plan was saved to: %s
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.
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 = `

View File

@ -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,
)),
)