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.
This commit is contained in:
Martin Atkins 2017-09-08 17:14:37 -07:00
parent 5221e51749
commit 0fe43c8977
10 changed files with 162 additions and 17 deletions

View File

@ -71,4 +71,13 @@ type CLIOpts struct {
// Validate. // Validate.
Input bool Input bool
Validation 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
} }

View File

@ -79,6 +79,15 @@ type Local struct {
// If this is nil, local performs normal state loading and storage. // If this is nil, local performs normal state loading and storage.
Backend backend.Backend 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 schema *schema.Backend
opLock sync.Mutex opLock sync.Mutex
once sync.Once once sync.Once

View File

@ -145,17 +145,23 @@ func (b *Local) opPlan(
b.renderPlan(dispPlan) 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,
))
} }
} }
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
) )
func TestLocal_planBasic(t *testing.T) { 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) { func TestLocal_planNoConfig(t *testing.T) {
b := TestLocal(t) b := TestLocal(t)
TestLocalProvider(t, b, "test") TestLocalProvider(t, b, "test")

View File

@ -11,6 +11,7 @@ func (b *Local) CLIInit(opts *backend.CLIOpts) error {
b.ContextOpts = opts.ContextOpts b.ContextOpts = opts.ContextOpts
b.OpInput = opts.Input b.OpInput = opts.Input
b.OpValidation = opts.Validation b.OpValidation = opts.Validation
b.RunningInAutomation = opts.RunningInAutomation
// Only configure state paths if we didn't do so via the configure func. // Only configure state paths if we didn't do so via the configure func.
if b.StatePath == "" { if b.StatePath == "" {

View File

@ -257,6 +257,12 @@ func (c *InitCommand) Run(args []string) int {
} }
c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitSuccess))) 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 return 0
} }
@ -536,7 +542,9 @@ with Terraform immediately by creating Terraform configuration files.
const outputInitSuccess = ` const outputInitSuccess = `
[reset][bold][green]Terraform has been successfully initialized![reset][green] [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 You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands any changes that are required for your infrastructure. All Terraform commands
should now work. should now work.

View File

@ -42,6 +42,19 @@ type Meta struct {
// ExtraHooks are extra hooks to add to the context. // ExtraHooks are extra hooks to add to the context.
ExtraHooks []terraform.Hook 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 // Protected: commands can set these
//---------------------------------------------------------- //----------------------------------------------------------

View File

@ -96,13 +96,14 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, error) {
// Setup the CLI opts we pass into backends that support it // Setup the CLI opts we pass into backends that support it
cliOpts := &backend.CLIOpts{ cliOpts := &backend.CLIOpts{
CLI: m.Ui, CLI: m.Ui,
CLIColor: m.Colorize(), CLIColor: m.Colorize(),
StatePath: m.statePath, StatePath: m.statePath,
StateOutPath: m.stateOutPath, StateOutPath: m.stateOutPath,
StateBackupPath: m.backupPath, StateBackupPath: m.backupPath,
ContextOpts: m.contextOpts(), ContextOpts: m.contextOpts(),
Input: m.Input(), Input: m.Input(),
RunningInAutomation: m.RunningInAutomation,
} }
// Don't validate if we have a plan. Validation is normally harmless here, // Don't validate if we have a plan. Validation is normally harmless here,

View File

@ -8,6 +8,11 @@ import (
"github.com/mitchellh/cli" "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. // Commands is the mapping of all the available Terraform commands.
var Commands map[string]cli.CommandFactory var Commands map[string]cli.CommandFactory
var PlumbingCommands map[string]struct{} var PlumbingCommands map[string]struct{}
@ -29,11 +34,18 @@ func init() {
Ui: &cli.BasicUi{Writer: os.Stdout}, Ui: &cli.BasicUi{Writer: os.Stdout},
} }
var inAutomation bool
if v := os.Getenv(runningInAutomationEnvName); v != "" {
inAutomation = true
}
meta := command.Meta{ meta := command.Meta{
Color: true, Color: true,
GlobalPluginDirs: globalPluginDirs(), GlobalPluginDirs: globalPluginDirs(),
PluginOverrides: &PluginOverrides, PluginOverrides: &PluginOverrides,
Ui: Ui, Ui: Ui,
RunningInAutomation: inAutomation,
} }
// The command list is included in the terraform -help // The command list is included in the terraform -help

View File

@ -74,6 +74,28 @@ and updated by subsequent runs. Selecting a backend that supports
[state locking](/docs/state/locking.html) will additionally provide safety [state locking](/docs/state/locking.html) will additionally provide safety
against race conditions that can be caused by concurrent Terraform runs. 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 ## Plan and Apply on different machines
When running in an orchestration tool, it can be difficult or impossible to When running in an orchestration tool, it can be difficult or impossible to