From 0fe43c8977a04a64c2dff43aa1c36f6ba0a39e21 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 8 Sep 2017 17:14:37 -0700 Subject: [PATCH] cli: allow disabling "next steps" message in terraform plan In #15884 we adjusted the plan output to give an explicit command to run to apply a plan, whereas before this command was just alluded to in the prose. Since releasing that, we've got good feedback that it's confusing to include such instructions when Terraform is running in a workflow automation tool, because such tools usually abstract away exactly what commands are run and require users to take different actions to proceed through the workflow. To accommodate such environments while retaining helpful messages for normal CLI usage, here we introduce a new environment variable TF_IN_AUTOMATION which, when set to a non-empty value, is a hint to Terraform that it isn't being run in an interactive command shell and it should thus tone down the "next steps" messaging. The documentation for this setting is included as part of the "...in automation" guide since it's not generally useful in other cases. We also intentionally disclaim comprehensive support for this since we want to avoid creating an extreme number of "if running in automation..." codepaths that would increase the testing matrix and hurt maintainability. The focus is specifically on the output of the three commands we give in the automation guide, which at present means the following two situations: * "terraform init" does not include the final paragraphs that suggest running "terraform plan" and tell you in what situations you might need to re-run "terraform init". * "terraform plan" does not include the final paragraphs that either warn about not specifying "-out=..." or instruct to run "terraform apply" with the generated plan file. --- backend/cli.go | 9 +++ backend/local/backend.go | 9 +++ backend/local/backend_plan.go | 26 +++++--- backend/local/backend_plan_test.go | 64 +++++++++++++++++++ backend/local/cli.go | 1 + command/init.go | 8 +++ command/meta.go | 13 ++++ command/meta_backend.go | 15 +++-- commands.go | 12 ++++ .../running-terraform-in-automation.html.md | 22 +++++++ 10 files changed, 162 insertions(+), 17 deletions(-) diff --git a/backend/cli.go b/backend/cli.go index 39935d406..40a66e698 100644 --- a/backend/cli.go +++ b/backend/cli.go @@ -71,4 +71,13 @@ type CLIOpts struct { // Validate. Input bool Validation bool + + // RunningInAutomation indicates that commands are being run by an + // automated system rather than directly at a command prompt. + // + // This is a hint not to produce messages that expect that a user can + // run a follow-up command, perhaps because Terraform is running in + // some sort of workflow automation tool that abstracts away the + // exact commands that are being run. + RunningInAutomation bool } diff --git a/backend/local/backend.go b/backend/local/backend.go index 054a4659b..edfc4b81c 100644 --- a/backend/local/backend.go +++ b/backend/local/backend.go @@ -79,6 +79,15 @@ type Local struct { // If this is nil, local performs normal state loading and storage. Backend backend.Backend + // RunningInAutomation indicates that commands are being run by an + // automated system rather than directly at a command prompt. + // + // This is a hint not to produce messages that expect that a user can + // run a follow-up command, perhaps because Terraform is running in + // some sort of workflow automation tool that abstracts away the + // exact commands that are being run. + RunningInAutomation bool + schema *schema.Backend opLock sync.Mutex once sync.Once diff --git a/backend/local/backend_plan.go b/backend/local/backend_plan.go index 905f89f1a..a4e92c1c7 100644 --- a/backend/local/backend_plan.go +++ b/backend/local/backend_plan.go @@ -145,17 +145,23 @@ func (b *Local) opPlan( b.renderPlan(dispPlan) - b.CLI.Output("\n------------------------------------------------------------------------") + // Give the user some next-steps, unless we're running in an automation + // tool which is presumed to provide its own UI for further actions. + if !b.RunningInAutomation { + + b.CLI.Output("\n------------------------------------------------------------------------") + + if path := op.PlanOutPath; path == "" { + b.CLI.Output(fmt.Sprintf( + "\n" + strings.TrimSpace(planHeaderNoOutput) + "\n", + )) + } else { + b.CLI.Output(fmt.Sprintf( + "\n"+strings.TrimSpace(planHeaderYesOutput)+"\n", + path, path, + )) + } - if path := op.PlanOutPath; path == "" { - b.CLI.Output(fmt.Sprintf( - "\n" + strings.TrimSpace(planHeaderNoOutput) + "\n", - )) - } else { - b.CLI.Output(fmt.Sprintf( - "\n"+strings.TrimSpace(planHeaderYesOutput)+"\n", - path, path, - )) } } } diff --git a/backend/local/backend_plan_test.go b/backend/local/backend_plan_test.go index b0d6419fa..b8698e8a4 100644 --- a/backend/local/backend_plan_test.go +++ b/backend/local/backend_plan_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" + "github.com/mitchellh/cli" ) func TestLocal_planBasic(t *testing.T) { @@ -38,6 +39,69 @@ func TestLocal_planBasic(t *testing.T) { } } +func TestLocal_planInAutomation(t *testing.T) { + b := TestLocal(t) + TestLocalProvider(t, b, "test") + + mod, modCleanup := module.TestTree(t, "./test-fixtures/plan") + defer modCleanup() + + const msg = `You didn't specify an "-out" parameter` + + // When we're "in automation" we omit certain text from the + // plan output. However, testing for the absense of text is + // unreliable in the face of future copy changes, so we'll + // mitigate that by running both with and without the flag + // set so we can ensure that the expected messages _are_ + // included the first time. + b.RunningInAutomation = false + b.CLI = cli.NewMockUi() + { + op := testOperationPlan() + op.Module = mod + op.PlanRefresh = true + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + <-run.Done() + if run.Err != nil { + t.Fatalf("unexpected error: %s", err) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, msg) { + t.Fatalf("missing next-steps message when not in automation") + } + } + + // On the second run, we expect the next-steps messaging to be absent + // since we're now "running in automation". + b.RunningInAutomation = true + b.CLI = cli.NewMockUi() + { + op := testOperationPlan() + op.Module = mod + op.PlanRefresh = true + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + <-run.Done() + if run.Err != nil { + t.Fatalf("unexpected error: %s", err) + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if strings.Contains(output, msg) { + t.Fatalf("next-steps message present when in automation") + } + } + +} + func TestLocal_planNoConfig(t *testing.T) { b := TestLocal(t) TestLocalProvider(t, b, "test") diff --git a/backend/local/cli.go b/backend/local/cli.go index d81d87d84..f9edfd449 100644 --- a/backend/local/cli.go +++ b/backend/local/cli.go @@ -11,6 +11,7 @@ func (b *Local) CLIInit(opts *backend.CLIOpts) error { b.ContextOpts = opts.ContextOpts b.OpInput = opts.Input b.OpValidation = opts.Validation + b.RunningInAutomation = opts.RunningInAutomation // Only configure state paths if we didn't do so via the configure func. if b.StatePath == "" { diff --git a/command/init.go b/command/init.go index 2de7d701d..986a73f51 100644 --- a/command/init.go +++ b/command/init.go @@ -257,6 +257,12 @@ func (c *InitCommand) Run(args []string) int { } c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess))) + if !c.RunningInAutomation { + // If we're not running in an automation wrapper, give the user + // some more detailed next steps that are appropriate for interactive + // shell usage. + c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccessCLI))) + } return 0 } @@ -536,7 +542,9 @@ with Terraform immediately by creating Terraform configuration files. const outputInitSuccess = ` [reset][bold][green]Terraform has been successfully initialized![reset][green] +` +const outputInitSuccessCLI = `[reset][green] You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. diff --git a/command/meta.go b/command/meta.go index aca94b1a8..a67a20c2e 100644 --- a/command/meta.go +++ b/command/meta.go @@ -42,6 +42,19 @@ type Meta struct { // ExtraHooks are extra hooks to add to the context. ExtraHooks []terraform.Hook + // RunningInAutomation indicates that commands are being run by an + // automated system rather than directly at a command prompt. + // + // This is a hint to various command routines that it may be confusing + // to print out messages that suggest running specific follow-up + // commands, since the user consuming the output will not be + // in a position to run such commands. + // + // The intended use-case of this flag is when Terraform is running in + // some sort of workflow orchestration tool which is abstracting away + // the specific commands being run. + RunningInAutomation bool + //---------------------------------------------------------- // Protected: commands can set these //---------------------------------------------------------- diff --git a/command/meta_backend.go b/command/meta_backend.go index 74422e1dc..4a52ef00e 100644 --- a/command/meta_backend.go +++ b/command/meta_backend.go @@ -96,13 +96,14 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) { // Setup the CLI opts we pass into backends that support it cliOpts := &backend.CLIOpts{ - CLI: m.Ui, - CLIColor: m.Colorize(), - StatePath: m.statePath, - StateOutPath: m.stateOutPath, - StateBackupPath: m.backupPath, - ContextOpts: m.contextOpts(), - Input: m.Input(), + CLI: m.Ui, + CLIColor: m.Colorize(), + StatePath: m.statePath, + StateOutPath: m.stateOutPath, + StateBackupPath: m.backupPath, + ContextOpts: m.contextOpts(), + Input: m.Input(), + RunningInAutomation: m.RunningInAutomation, } // Don't validate if we have a plan. Validation is normally harmless here, diff --git a/commands.go b/commands.go index 910245a68..80cf878bf 100644 --- a/commands.go +++ b/commands.go @@ -8,6 +8,11 @@ import ( "github.com/mitchellh/cli" ) +// runningInAutomationEnvName gives the name of an environment variable that +// can be set to any non-empty value in order to suppress certain messages +// that assume that Terraform is being run from a command prompt. +const runningInAutomationEnvName = "TF_IN_AUTOMATION" + // Commands is the mapping of all the available Terraform commands. var Commands map[string]cli.CommandFactory var PlumbingCommands map[string]struct{} @@ -29,11 +34,18 @@ func init() { Ui: &cli.BasicUi{Writer: os.Stdout}, } + var inAutomation bool + if v := os.Getenv(runningInAutomationEnvName); v != "" { + inAutomation = true + } + meta := command.Meta{ Color: true, GlobalPluginDirs: globalPluginDirs(), PluginOverrides: &PluginOverrides, Ui: Ui, + + RunningInAutomation: inAutomation, } // The command list is included in the terraform -help diff --git a/website/guides/running-terraform-in-automation.html.md b/website/guides/running-terraform-in-automation.html.md index 8e75cd509..856f9fb19 100644 --- a/website/guides/running-terraform-in-automation.html.md +++ b/website/guides/running-terraform-in-automation.html.md @@ -74,6 +74,28 @@ and updated by subsequent runs. Selecting a backend that supports [state locking](/docs/state/locking.html) will additionally provide safety against race conditions that can be caused by concurrent Terraform runs. +## Controlling Terraform Output in Automation + +By default, some Terraform commands conclude by presenting a description +of a possible next step to the user, often including a specific command +to run next. + +An automation tool will often abstract away the details of exactly which +commands are being run, causing these messages to be confusing and +un-actionable, and possibly harmful if they inadvertently encourage a user to +bypass the automation tool entirely. + +When the environment variable `TF_IN_AUTOMATION` is set to any non-empty +value, Terraform makes some minor adjustments to its output to de-emphasize +specific commands to run. The specific changes made will vary over time, +but generally-speaking Terraform will consider this variable to indicate that +there is some wrapping application that will help the user with the next +step. + +To reduce complexity, this feature is implemented primarily for the main +workflow commands described above. Other ancillary commands may still produce +command line suggestions, regardless of this setting. + ## Plan and Apply on different machines When running in an orchestration tool, it can be difficult or impossible to