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