backend/local: Use terminal properties to tweak the plan output
We now require the output to accept UTF-8 and we can determine how wide the terminal (if any) is, so here we begin to make use of that for the "terraform plan" command. The horizontal rule is now made of box drawing characters instead of hyphens and fills the whole terminal width. The paragraphs of text in the output are now also wrapped to fill the terminal width, instead of the hard-wrapping we did before. This is just a start down the road of making better use of the terminal capabilities. Lots of other commands could benefit from updates like these too.
This commit is contained in:
parent
d2c3403ab6
commit
e6a516d87e
|
@ -32,6 +32,8 @@ func (b *Local) opPlan(
|
||||||
|
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
outputColumns := b.outputColumns()
|
||||||
|
|
||||||
if op.PlanFile != nil {
|
if op.PlanFile != nil {
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
|
@ -150,6 +152,7 @@ func (b *Local) opPlan(
|
||||||
|
|
||||||
if runningOp.PlanEmpty {
|
if runningOp.PlanEmpty {
|
||||||
b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges)))
|
b.CLI.Output("\n" + b.Colorize().Color(strings.TrimSpace(planNoChanges)))
|
||||||
|
b.CLI.Output("\n" + strings.TrimSpace(format.WordWrap(planNoChangesDetail, outputColumns)))
|
||||||
// Even if there are no changes, there still could be some warnings
|
// Even if there are no changes, there still could be some warnings
|
||||||
b.ShowDiagnostics(diags)
|
b.ShowDiagnostics(diags)
|
||||||
return
|
return
|
||||||
|
@ -166,15 +169,15 @@ func (b *Local) opPlan(
|
||||||
// tool which is presumed to provide its own UI for further actions.
|
// tool which is presumed to provide its own UI for further actions.
|
||||||
if !b.RunningInAutomation {
|
if !b.RunningInAutomation {
|
||||||
|
|
||||||
b.CLI.Output("\n------------------------------------------------------------------------")
|
b.outputHorizRule()
|
||||||
|
|
||||||
if path := op.PlanOutPath; path == "" {
|
if path := op.PlanOutPath; path == "" {
|
||||||
b.CLI.Output(fmt.Sprintf(
|
b.CLI.Output(fmt.Sprintf(
|
||||||
"\n" + strings.TrimSpace(planHeaderNoOutput) + "\n",
|
"\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, outputColumns)) + "\n",
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
b.CLI.Output(fmt.Sprintf(
|
b.CLI.Output(fmt.Sprintf(
|
||||||
"\n"+strings.TrimSpace(planHeaderYesOutput)+"\n",
|
"\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, outputColumns))+"\n",
|
||||||
path, path,
|
path, path,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -183,7 +186,7 @@ func (b *Local) opPlan(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Local) renderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas) {
|
func (b *Local) renderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas) {
|
||||||
RenderPlan(plan, baseState, schemas, b.CLI, b.Colorize())
|
RenderPlan(plan, baseState, schemas, b.CLI, b.Colorize(), b.outputColumns())
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderPlan renders the given plan to the given UI.
|
// RenderPlan renders the given plan to the given UI.
|
||||||
|
@ -206,7 +209,7 @@ func (b *Local) renderPlan(plan *plans.Plan, baseState *states.State, schemas *t
|
||||||
// output values will not currently be rendered because their prior values
|
// output values will not currently be rendered because their prior values
|
||||||
// are currently stored only in the prior state. (see the docstring for
|
// are currently stored only in the prior state. (see the docstring for
|
||||||
// func planHasSideEffects for why this is and when that might change)
|
// func planHasSideEffects for why this is and when that might change)
|
||||||
func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize) {
|
func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Schemas, ui cli.Ui, colorize *colorstring.Colorize, width int) {
|
||||||
counts := map[plans.Action]int{}
|
counts := map[plans.Action]int{}
|
||||||
var rChanges []*plans.ResourceInstanceChangeSrc
|
var rChanges []*plans.ResourceInstanceChangeSrc
|
||||||
for _, change := range plan.Changes.Resources {
|
for _, change := range plan.Changes.Resources {
|
||||||
|
@ -220,7 +223,7 @@ func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Sc
|
||||||
}
|
}
|
||||||
|
|
||||||
headerBuf := &bytes.Buffer{}
|
headerBuf := &bytes.Buffer{}
|
||||||
fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(planHeaderIntro))
|
fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, width)))
|
||||||
if counts[plans.Create] > 0 {
|
if counts[plans.Create] > 0 {
|
||||||
fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create))
|
fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create))
|
||||||
}
|
}
|
||||||
|
@ -330,18 +333,15 @@ func RenderPlan(plan *plans.Plan, baseState *states.State, schemas *terraform.Sc
|
||||||
}
|
}
|
||||||
|
|
||||||
const planHeaderIntro = `
|
const planHeaderIntro = `
|
||||||
An execution plan has been generated and is shown below.
|
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
|
||||||
Resource actions are indicated with the following symbols:
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const planHeaderNoOutput = `
|
const planHeaderNoOutput = `
|
||||||
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
|
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
|
||||||
can't guarantee that exactly these actions will be performed if
|
|
||||||
"terraform apply" is subsequently run.
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const planHeaderYesOutput = `
|
const planHeaderYesOutput = `
|
||||||
This plan was saved to: %s
|
Saved the plan to: %s
|
||||||
|
|
||||||
To perform exactly these actions, run the following command to apply:
|
To perform exactly these actions, run the following command to apply:
|
||||||
terraform apply %q
|
terraform apply %q
|
||||||
|
@ -349,8 +349,8 @@ To perform exactly these actions, run the following command to apply:
|
||||||
|
|
||||||
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
|
|
||||||
configuration and real physical resources that exist. As a result, no
|
const planNoChangesDetail = `
|
||||||
actions need to be performed.
|
That Terraform did not detect any differences between your configuration and the remote system(s). As a result, there are no actions to take.
|
||||||
`
|
`
|
||||||
|
|
|
@ -51,7 +51,7 @@ func TestLocal_planInAutomation(t *testing.T) {
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
TestLocalProvider(t, b, "test", planFixtureSchema())
|
TestLocalProvider(t, b, "test", planFixtureSchema())
|
||||||
|
|
||||||
const msg = `You didn't specify an "-out" parameter`
|
const msg = `You didn't use the -out option`
|
||||||
|
|
||||||
// When we're "in automation" we omit certain text from the
|
// When we're "in automation" we omit certain text from the
|
||||||
// plan output. However, testing for the absense of text is
|
// plan output. However, testing for the absense of text is
|
||||||
|
@ -77,7 +77,7 @@ func TestLocal_planInAutomation(t *testing.T) {
|
||||||
|
|
||||||
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
output := b.CLI.(*cli.MockUi).OutputWriter.String()
|
||||||
if !strings.Contains(output, msg) {
|
if !strings.Contains(output, msg) {
|
||||||
t.Fatalf("missing next-steps message when not in automation")
|
t.Fatalf("missing next-steps message when not in automation\nwant: %s\noutput:\n%s", msg, output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,8 +331,8 @@ func TestLocal_planTainted(t *testing.T) {
|
||||||
t.Fatal("plan should not be empty")
|
t.Fatal("plan should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedOutput := `An execution plan has been generated and is shown below.
|
expectedOutput := `Terraform used the selected providers to generate the following execution
|
||||||
Resource actions are indicated with the following symbols:
|
plan. Resource actions are indicated with the following symbols:
|
||||||
-/+ destroy and then create replacement
|
-/+ destroy and then create replacement
|
||||||
|
|
||||||
Terraform will perform the following actions:
|
Terraform will perform the following actions:
|
||||||
|
@ -433,8 +433,8 @@ func TestLocal_planDeposedOnly(t *testing.T) {
|
||||||
// it's also possible for there to be _multiple_ deposed objects, in the
|
// it's also possible for there to be _multiple_ deposed objects, in the
|
||||||
// unlikely event that create_before_destroy _keeps_ crashing across
|
// unlikely event that create_before_destroy _keeps_ crashing across
|
||||||
// subsequent runs.
|
// subsequent runs.
|
||||||
expectedOutput := `An execution plan has been generated and is shown below.
|
expectedOutput := `Terraform used the selected providers to generate the following execution
|
||||||
Resource actions are indicated with the following symbols:
|
plan. Resource actions are indicated with the following symbols:
|
||||||
+ create
|
+ create
|
||||||
- destroy
|
- destroy
|
||||||
|
|
||||||
|
@ -507,8 +507,8 @@ func TestLocal_planTainted_createBeforeDestroy(t *testing.T) {
|
||||||
t.Fatal("plan should not be empty")
|
t.Fatal("plan should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedOutput := `An execution plan has been generated and is shown below.
|
expectedOutput := `Terraform used the selected providers to generate the following execution
|
||||||
Resource actions are indicated with the following symbols:
|
plan. Resource actions are indicated with the following symbols:
|
||||||
+/- create replacement and then destroy
|
+/- create replacement and then destroy
|
||||||
|
|
||||||
Terraform will perform the following actions:
|
Terraform will perform the following actions:
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/command/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
// backend.CLI impl.
|
// backend.CLI impl.
|
||||||
|
@ -63,3 +64,17 @@ func (b *Local) errorColumns() int {
|
||||||
}
|
}
|
||||||
return b.Streams.Stderr.Columns()
|
return b.Streams.Stderr.Columns()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outputHorizRule will call b.CLI.Output with enough horizontal line
|
||||||
|
// characters to fill an entire row of output.
|
||||||
|
//
|
||||||
|
// This function does nothing if the backend doesn't have a CLI attached.
|
||||||
|
//
|
||||||
|
// If UI color is enabled, the rule will get a dark grey coloring to try to
|
||||||
|
// visually de-emphasize it.
|
||||||
|
func (b *Local) outputHorizRule() {
|
||||||
|
if b.CLI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.CLI.Output(format.HorizontalRule(b.CLIColor, b.outputColumns()))
|
||||||
|
}
|
||||||
|
|
|
@ -59,8 +59,8 @@ func TestPrimarySeparatePlan(t *testing.T) {
|
||||||
t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout)
|
t.Errorf("incorrect plan tally; want 1 to add:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stdout, "This plan was saved to: tfplan") {
|
if !strings.Contains(stdout, "Saved the plan to: tfplan") {
|
||||||
t.Errorf("missing \"This plan was saved to...\" message in plan output\n%s", stdout)
|
t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
|
||||||
}
|
}
|
||||||
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
|
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
|
||||||
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
|
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
|
||||||
|
@ -169,8 +169,8 @@ func TestPrimaryChdirOption(t *testing.T) {
|
||||||
t.Errorf("incorrect plan tally; want 0 to add:\n%s", stdout)
|
t.Errorf("incorrect plan tally; want 0 to add:\n%s", stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(stdout, "This plan was saved to: tfplan") {
|
if !strings.Contains(stdout, "Saved the plan to: tfplan") {
|
||||||
t.Errorf("missing \"This plan was saved to...\" message in plan output\n%s", stdout)
|
t.Errorf("missing \"Saved the plan to...\" message in plan output\n%s", stdout)
|
||||||
}
|
}
|
||||||
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
|
if !strings.Contains(stdout, "terraform apply \"tfplan\"") {
|
||||||
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
|
t.Errorf("missing next-step instruction in plan output\n%s", stdout)
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/colorstring"
|
||||||
|
wordwrap "github.com/mitchellh/go-wordwrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HorizontalRule returns a newline character followed by a number of
|
||||||
|
// horizontal line characters to fill the given width.
|
||||||
|
//
|
||||||
|
// If the given colorize has colors enabled, the rule will also be given a
|
||||||
|
// dark grey color to attempt to visually de-emphasize it for sighted users.
|
||||||
|
//
|
||||||
|
// This is intended for printing to the UI via mitchellh/cli.UI.Output, or
|
||||||
|
// similar, which will automatically append a trailing newline too.
|
||||||
|
func HorizontalRule(color *colorstring.Colorize, width int) string {
|
||||||
|
rule := strings.Repeat("─", width)
|
||||||
|
if color == nil { // sometimes unit tests don't populate this properly
|
||||||
|
return "\n" + rule
|
||||||
|
}
|
||||||
|
return color.Color("[dark_gray]\n" + rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordWrap takes a string containing unbroken lines of text and inserts
|
||||||
|
// newline characters to try to make the text fit within the given width.
|
||||||
|
//
|
||||||
|
// The string can already contain newline characters, for example if you are
|
||||||
|
// trying to render multiple paragraphs of text. (In that case, our usual
|
||||||
|
// style would be to have _two_ newline characters as the paragraph separator.)
|
||||||
|
//
|
||||||
|
// As a special case, any line that begins with at least one space will be left
|
||||||
|
// unbroken. This allows including literal segments in the output, such as
|
||||||
|
// code snippets or filenames, where word wrapping would be confusing.
|
||||||
|
func WordWrap(str string, width int) string {
|
||||||
|
if width == 0 {
|
||||||
|
// Silly edge case. We'll just return the original string to avoid
|
||||||
|
// panicking or doing other weird stuff.
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
lines := strings.Split(str, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if !strings.HasPrefix(line, " ") {
|
||||||
|
line = wordwrap.WrapString(line, uint(width))
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte('\n') // reintroduce the newlines we skipped in Scan
|
||||||
|
}
|
||||||
|
buf.WriteString(line)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
|
@ -629,6 +629,8 @@ func (m *Meta) showDiagnostics(vals ...interface{}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outputWidth := m.ErrorColumns()
|
||||||
|
|
||||||
diags = diags.ConsolidateWarnings(1)
|
diags = diags.ConsolidateWarnings(1)
|
||||||
|
|
||||||
// Since warning messages are generally competing
|
// Since warning messages are generally competing
|
||||||
|
@ -654,11 +656,7 @@ func (m *Meta) showDiagnostics(vals ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, diag := range diags {
|
for _, diag := range diags {
|
||||||
// TODO: Actually measure the terminal width and pass it here.
|
msg := format.Diagnostic(diag, m.configSources(), m.Colorize(), outputWidth)
|
||||||
// For now, we don't have easy access to the writer that
|
|
||||||
// ui.Error (etc) are writing to and thus can't interrogate
|
|
||||||
// to see if it's a terminal and what size it is.
|
|
||||||
msg := format.Diagnostic(diag, m.configSources(), m.Colorize(), 78)
|
|
||||||
switch diag.Severity() {
|
switch diag.Severity() {
|
||||||
case tfdiags.Error:
|
case tfdiags.Error:
|
||||||
m.Ui.Error(msg)
|
m.Ui.Error(msg)
|
||||||
|
|
|
@ -166,7 +166,7 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
// package rather than in the backends themselves, but for now we're
|
// package rather than in the backends themselves, but for now we're
|
||||||
// accepting this oddity because "terraform show" is a less commonly
|
// accepting this oddity because "terraform show" is a less commonly
|
||||||
// used way to render a plan than "terraform plan" is.
|
// used way to render a plan than "terraform plan" is.
|
||||||
localBackend.RenderPlan(plan, stateFile.State, schemas, c.Ui, c.Colorize())
|
localBackend.RenderPlan(plan, stateFile.State, schemas, c.Ui, c.Colorize(), c.OutputColumns())
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue