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:
parent
5221e51749
commit
0fe43c8977
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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 == "" {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
//----------------------------------------------------------
|
//----------------------------------------------------------
|
||||||
|
|
|
@ -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,
|
||||||
|
|
12
commands.go
12
commands.go
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue