diff --git a/backend/local/backend_apply.go b/backend/local/backend_apply.go index 608c44411..446343181 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 83d1733de..905f89f1a 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,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 = ` diff --git a/command/format/plan.go b/command/format/plan.go index 1220b85a5..728345e0a 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, )), )