Skip to content

Commit

Permalink
command: various adjustments to the diff presentation
Browse files Browse the repository at this point in the history
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_.
  • Loading branch information
apparentlymart committed Sep 2, 2017
1 parent 892f60e commit 83414be
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 62 deletions.
26 changes: 4 additions & 22 deletions backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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.
`
92 changes: 61 additions & 31 deletions backend/local/backend_plan.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package local

import (
"bytes"
"context"
"fmt"
"log"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = `
Expand All @@ -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 = `
Expand Down
42 changes: 33 additions & 9 deletions command/format/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -235,40 +263,36 @@ 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)")
}

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

Expand Down

0 comments on commit 83414be

Please sign in to comment.