From ca23a096d8c48544b9bfc6dbf13c66488f9b6964 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Tue, 2 Feb 2021 10:35:45 -0500 Subject: [PATCH 1/2] cli: Remove legacy positional path arguments Several commands continued to support the legacy positional path argument to specify a working directory. This functionality has been replaced with the global -chdir flag, which is specified before any other arguments, including the sub-command name. This commit removes support for the trailing path parameter from most commands. The only command which still supports a path argument is fmt, which also supports "-" to indicate receiving configuration from standard input. Any invocation of a command with an invalid trailing path parameter will result in a short error message, pointing at the -chdir alternative. There are many test updates in this commit, almost all of which are migrations from using positional arguments to specify a working directory. Because of the layer at which these tests run, we are unable to use the -chdir argument, so the churn in test files is larger than ideal. Sorry! --- command/apply.go | 22 +++-- command/apply_test.go | 191 +++++++++++++++++++++++++----------- command/command.go | 24 ++--- command/console.go | 5 +- command/console_test.go | 42 ++++---- command/get_test.go | 37 ++----- command/graph.go | 34 ++++--- command/graph_test.go | 22 ++--- command/init.go | 28 +----- command/init_test.go | 153 ++++++++--------------------- command/plan.go | 17 +--- command/plan_test.go | 153 +++++++++++++++-------------- command/refresh.go | 2 +- command/refresh_test.go | 93 +++++++++++++----- command/unlock.go | 6 +- command/unlock_test.go | 2 +- command/workspace_delete.go | 6 +- command/workspace_list.go | 2 +- command/workspace_new.go | 4 +- command/workspace_select.go | 4 +- 20 files changed, 425 insertions(+), 422 deletions(-) diff --git a/command/apply.go b/command/apply.go index c6cfe9a86..3d7515341 100644 --- a/command/apply.go +++ b/command/apply.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/hashicorp/terraform/backend" + "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/repl" "github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/tfdiags" @@ -50,6 +51,12 @@ func (c *ApplyCommand) Run(args []string) int { var diags tfdiags.Diagnostics args = cmdFlags.Args() + var planPath string + if len(args) > 0 { + planPath = args[0] + args = args[1:] + } + configPath, err := ModulePath(args) if err != nil { c.Ui.Error(err.Error()) @@ -62,11 +69,14 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } - // Check if the path is a plan - planFile, err := c.PlanFile(configPath) - if err != nil { - c.Ui.Error(err.Error()) - return 1 + // Try to load plan if path is specified + var planFile *planfile.Reader + if planPath != "" { + planFile, err = c.PlanFile(planPath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } } if c.Destroy && planFile != nil { c.Ui.Error("Destroy can't be called with a plan file.") @@ -297,7 +307,7 @@ Options: func (c *ApplyCommand) helpDestroy() string { helpText := ` -Usage: terraform destroy [options] [DIR] +Usage: terraform destroy [options] Destroy Terraform-managed infrastructure. diff --git a/command/apply_test.go b/command/apply_test.go index 407be0afd..209f6b557 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -26,6 +26,12 @@ import ( ) func TestApply(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) p := applyFixtureProvider() @@ -41,7 +47,6 @@ func TestApply(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -59,6 +64,12 @@ func TestApply(t *testing.T) { // test apply with locked state func TestApply_lockedState(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) unlock, err := testLockState(testDataDir, statePath) @@ -79,7 +90,6 @@ func TestApply_lockedState(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply"), } if code := c.Run(args); code == 0 { t.Fatal("expected error") @@ -93,6 +103,12 @@ func TestApply_lockedState(t *testing.T) { // test apply with locked state, waiting for unlock func TestApply_lockedStateWait(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) unlock, err := testLockState(testDataDir, statePath) @@ -121,7 +137,6 @@ func TestApply_lockedStateWait(t *testing.T) { "-state", statePath, "-lock-timeout", "4s", "-auto-approve", - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("lock should have succeeded in less than 3s: %s", ui.ErrorWriter) @@ -157,6 +172,12 @@ func (t *hwm) Max() int { } func TestApply_parallelism(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("parallelism"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) par := 4 @@ -217,7 +238,6 @@ func TestApply_parallelism(t *testing.T) { "-state", statePath, "-auto-approve", fmt.Sprintf("-parallelism=%d", par), - testFixturePath("parallelism"), } // Run in a goroutine. We can get any errors from the ui.OutputWriter @@ -258,6 +278,12 @@ func TestApply_parallelism(t *testing.T) { } func TestApply_configInvalid(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-config-invalid"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ @@ -270,7 +296,6 @@ func TestApply_configInvalid(t *testing.T) { args := []string{ "-state", testTempFile(t), "-auto-approve", - testFixturePath("apply-config-invalid"), } if code := c.Run(args); code != 1 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) @@ -278,7 +303,12 @@ func TestApply_configInvalid(t *testing.T) { } func TestApply_defaultState(t *testing.T) { - td := testTempDir(t) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := filepath.Join(td, DefaultStateFilename) // Change to the temporary directory @@ -308,7 +338,6 @@ func TestApply_defaultState(t *testing.T) { args := []string{ "-auto-approve", - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -325,6 +354,12 @@ func TestApply_defaultState(t *testing.T) { } func TestApply_error(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-error"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) p := testProvider() @@ -376,7 +411,6 @@ func TestApply_error(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply-error"), } if ui.ErrorWriter != nil { t.Logf("stdout:\n%s", ui.OutputWriter.String()) @@ -400,6 +434,12 @@ func TestApply_error(t *testing.T) { } func TestApply_input(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-input"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + // Disable test mode so input would be asked test = false defer func() { test = true }() @@ -426,7 +466,6 @@ func TestApply_input(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply-input"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -444,6 +483,12 @@ result = foo // When only a partial set of the variables are set, Terraform // should still ask for the unset ones by default (with -input=true) func TestApply_inputPartial(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-input-partial"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + // Disable test mode so input would be asked test = false defer func() { test = true }() @@ -467,7 +512,6 @@ func TestApply_inputPartial(t *testing.T) { "-state", statePath, "-auto-approve", "-var", "foo=foovalue", - testFixturePath("apply-input-partial"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -484,14 +528,11 @@ foo = foovalue } func TestApply_noArgs(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(testFixturePath("apply")); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() statePath := testTempFile(t) @@ -517,9 +558,6 @@ func TestApply_noArgs(t *testing.T) { } state := testStateRead(t, statePath) - if err != nil { - t.Fatalf("err: %s", err) - } if state == nil { t.Fatal("state should not be nil") } @@ -800,6 +838,12 @@ func TestApply_planNoModuleFiles(t *testing.T) { } func TestApply_refresh(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ @@ -831,7 +875,6 @@ func TestApply_refresh(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -861,6 +904,12 @@ func TestApply_refresh(t *testing.T) { } func TestApply_shutdown(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-shutdown"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + cancelled := make(chan struct{}) shutdownCh := make(chan struct{}) @@ -920,7 +969,6 @@ func TestApply_shutdown(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply-shutdown"), } if code := c.Run(args); code != 1 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -943,6 +991,12 @@ func TestApply_shutdown(t *testing.T) { } func TestApply_state(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ @@ -986,7 +1040,6 @@ func TestApply_state(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1031,6 +1084,12 @@ func TestApply_state(t *testing.T) { } func TestApply_stateNoExist(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + p := applyFixtureProvider() ui := new(cli.MockUi) c := &ApplyCommand{ @@ -1042,7 +1101,6 @@ func TestApply_stateNoExist(t *testing.T) { args := []string{ "idontexist.tfstate", - testFixturePath("apply"), } if code := c.Run(args); code != 1 { t.Fatalf("bad: \n%s", ui.OutputWriter.String()) @@ -1050,6 +1108,12 @@ func TestApply_stateNoExist(t *testing.T) { } func TestApply_sensitiveOutput(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-sensitive-output"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ @@ -1064,7 +1128,6 @@ func TestApply_sensitiveOutput(t *testing.T) { args := []string{ "-state", statePath, "-auto-approve", - testFixturePath("apply-sensitive-output"), } if code := c.Run(args); code != 0 { @@ -1081,6 +1144,12 @@ func TestApply_sensitiveOutput(t *testing.T) { } func TestApply_vars(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) p := testProvider() @@ -1120,7 +1189,6 @@ func TestApply_vars(t *testing.T) { "-auto-approve", "-var", "foo=bar", "-state", statePath, - testFixturePath("apply-vars"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1132,6 +1200,12 @@ func TestApply_vars(t *testing.T) { } func TestApply_varFile(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + varFilePath := testTempFile(t) if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) @@ -1176,7 +1250,6 @@ func TestApply_varFile(t *testing.T) { "-auto-approve", "-var-file", varFilePath, "-state", statePath, - testFixturePath("apply-vars"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1188,23 +1261,19 @@ func TestApply_varFile(t *testing.T) { } func TestApply_varFileDefault(t *testing.T) { - varFileDir := testTempDir(t) - varFilePath := filepath.Join(varFileDir, "terraform.tfvars") + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + varFilePath := filepath.Join(td, "terraform.tfvars") if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } statePath := testTempFile(t) - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(varFileDir); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) - p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ @@ -1241,7 +1310,6 @@ func TestApply_varFileDefault(t *testing.T) { args := []string{ "-auto-approve", "-state", statePath, - testFixturePath("apply-vars"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1253,23 +1321,19 @@ func TestApply_varFileDefault(t *testing.T) { } func TestApply_varFileDefaultJSON(t *testing.T) { - varFileDir := testTempDir(t) - varFilePath := filepath.Join(varFileDir, "terraform.tfvars.json") + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + varFilePath := filepath.Join(td, "terraform.tfvars.json") if err := ioutil.WriteFile(varFilePath, []byte(applyVarFileJSON), 0644); err != nil { t.Fatalf("err: %s", err) } statePath := testTempFile(t) - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(varFileDir); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) - p := testProvider() ui := new(cli.MockUi) c := &ApplyCommand{ @@ -1306,7 +1370,6 @@ func TestApply_varFileDefaultJSON(t *testing.T) { args := []string{ "-auto-approve", "-state", statePath, - testFixturePath("apply-vars"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1318,6 +1381,12 @@ func TestApply_varFileDefaultJSON(t *testing.T) { } func TestApply_backup(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ @@ -1358,7 +1427,6 @@ func TestApply_backup(t *testing.T) { "-auto-approve", "-state", statePath, "-backup", backupPath, - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1389,6 +1457,12 @@ func TestApply_backup(t *testing.T) { } func TestApply_disableBackup(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := testState() statePath := testStateFile(t, originalState) @@ -1412,7 +1486,6 @@ func TestApply_disableBackup(t *testing.T) { "-auto-approve", "-state", statePath, "-backup", "-", - testFixturePath("apply"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1462,6 +1535,12 @@ func TestApply_disableBackup(t *testing.T) { // Test that the Terraform env is passed through func TestApply_terraformEnv(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-terraform-env"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + statePath := testTempFile(t) p := testProvider() @@ -1476,7 +1555,6 @@ func TestApply_terraformEnv(t *testing.T) { args := []string{ "-auto-approve", "-state", statePath, - testFixturePath("apply-terraform-env"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -1495,7 +1573,7 @@ output = default func TestApply_terraformEnvNonDefault(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) - os.MkdirAll(td, 0755) + testCopyDir(t, testFixturePath("apply-terraform-env"), td) defer os.RemoveAll(td) defer testChdir(t, td)() @@ -1531,7 +1609,6 @@ func TestApply_terraformEnvNonDefault(t *testing.T) { args := []string{ "-auto-approve", - testFixturePath("apply-terraform-env"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) diff --git a/command/command.go b/command/command.go index 8976f21a9..41748d652 100644 --- a/command/command.go +++ b/command/command.go @@ -47,29 +47,25 @@ is configured to use a non-local backend. This backend doesn't support this operation. ` -// ModulePath returns the path to the root module from the CLI args. +// ModulePath returns the path to the root module and validates CLI arguments. // -// This centralizes the logic for any commands that expect a module path -// on their CLI args. This will verify that only one argument is given -// and that it is a path to configuration. +// This centralizes the logic for any commands that previously accepted +// a module path via CLI arguments. This will error if any extraneous arguments +// are given and suggest using the -chdir flag instead. // // If your command accepts more than one arg, then change the slice bounds // to pass validation. func ModulePath(args []string) (string, error) { // TODO: test - if len(args) > 1 { - return "", fmt.Errorf("Too many command line arguments. Configuration path expected.") + if len(args) > 0 { + return "", fmt.Errorf("Too many command line arguments. Did you mean to use -chdir?") } - if len(args) == 0 { - path, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("Error getting pwd: %s", err) - } - - return path, nil + path, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("Error getting pwd: %s", err) } - return args[0], nil + return path, nil } diff --git a/command/console.go b/command/console.go index 45cb23eee..75d8edc64 100644 --- a/command/console.go +++ b/command/console.go @@ -175,7 +175,7 @@ func (c *ConsoleCommand) modePiped(session *repl.Session, ui cli.Ui) int { func (c *ConsoleCommand) Help() string { helpText := ` -Usage: terraform console [options] [DIR] +Usage: terraform console [options] Starts an interactive console for experimenting with Terraform interpolations. @@ -187,9 +187,6 @@ Usage: terraform console [options] [DIR] This command will never modify your state. - DIR can be set to a directory with a Terraform state to load. By - default, this will default to the current working directory. - Options: -state=path Path to read state. Defaults to "terraform.tfstate" diff --git a/command/console_test.go b/command/console_test.go index b4fa2915c..8fb63ea7e 100644 --- a/command/console_test.go +++ b/command/console_test.go @@ -52,11 +52,13 @@ func TestConsole_basic(t *testing.T) { } func TestConsole_tfvars(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() // Write a terraform.tvars - varFilePath := filepath.Join(tmp, "terraform.tfvars") + varFilePath := filepath.Join(td, "terraform.tfvars") if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } @@ -85,9 +87,7 @@ func TestConsole_tfvars(t *testing.T) { defer testStdinPipe(t, strings.NewReader("var.foo\n"))() outCloser := testStdoutCapture(t, &output) - args := []string{ - testFixturePath("apply-vars"), - } + args := []string{} code := c.Run(args) outCloser() if code != 0 { @@ -106,9 +106,13 @@ func TestConsole_unsetRequiredVars(t *testing.T) { // "terraform console" producing an interactive prompt for those variables // or producing errors. Instead, it should allow evaluation in that // partial context but see the unset variables values as being unknown. - - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // + // This test fixture includes variable "foo" {}, which we are + // intentionally not setting here. + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() p := testProvider() p.GetSchemaResponse = &providers.GetSchemaResponse{ @@ -134,11 +138,7 @@ func TestConsole_unsetRequiredVars(t *testing.T) { defer testStdinPipe(t, strings.NewReader("var.foo\n"))() outCloser := testStdoutCapture(t, &output) - args := []string{ - // This test fixture includes variable "foo" {}, which we are - // intentionally not setting here. - testFixturePath("apply-vars"), - } + args := []string{} code := c.Run(args) outCloser() @@ -152,8 +152,10 @@ func TestConsole_unsetRequiredVars(t *testing.T) { } func TestConsole_variables(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("variables"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() p := testProvider() ui := cli.NewMockUi() @@ -171,9 +173,7 @@ func TestConsole_variables(t *testing.T) { "local.snack_bar\n": "[\n \"popcorn\",\n (sensitive),\n]\n", } - args := []string{ - testFixturePath("variables"), - } + args := []string{} for cmd, val := range commands { var output bytes.Buffer @@ -214,9 +214,7 @@ func TestConsole_modules(t *testing.T) { "local.foo\n": "3\n", } - args := []string{ - testFixturePath("modules"), - } + args := []string{} for cmd, val := range commands { var output bytes.Buffer diff --git a/command/get_test.go b/command/get_test.go index 013b9779d..7d9137425 100644 --- a/command/get_test.go +++ b/command/get_test.go @@ -9,8 +9,10 @@ import ( ) func TestGet(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("get"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() ui := new(cli.MockUi) c := &GetCommand{ @@ -21,9 +23,7 @@ func TestGet(t *testing.T) { }, } - args := []string{ - testFixturePath("get"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } @@ -53,7 +53,7 @@ func TestGet_multipleArgs(t *testing.T) { } } -func TestGet_noArgs(t *testing.T) { +func TestGet_update(t *testing.T) { td := tempDir(t) testCopyDir(t, testFixturePath("get"), td) defer os.RemoveAll(td) @@ -68,33 +68,8 @@ func TestGet_noArgs(t *testing.T) { }, } - args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if !strings.Contains(output, "- foo in") { - t.Fatalf("doesn't look like get: %s", output) - } -} - -func TestGet_update(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - ui := new(cli.MockUi) - c := &GetCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - dataDir: tempDir(t), - }, - } - args := []string{ "-update", - testFixturePath("get"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) diff --git a/command/graph.go b/command/graph.go index 796478ded..2267c891f 100644 --- a/command/graph.go +++ b/command/graph.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/hashicorp/terraform/plans" + "github.com/hashicorp/terraform/plans/planfile" "github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/backend" @@ -36,7 +36,13 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - configPath, err := ModulePath(cmdFlags.Args()) + args = cmdFlags.Args() + var planPath string + if len(args) > 0 { + planPath = args[0] + args = args[1:] + } + configPath, err := ModulePath(args) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -48,16 +54,14 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - // Check if the path is a plan - var plan *plans.Plan - planFile, err := c.PlanFile(configPath) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - if planFile != nil { - // Reset for backend loading - configPath = "" + // Try to load plan if path is specified + var planFile *planfile.Reader + if planPath != "" { + planFile, err = c.PlanFile(planPath) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } } var diags tfdiags.Diagnostics @@ -112,7 +116,7 @@ func (c *GraphCommand) Run(args []string) int { // Determine the graph type graphType := terraform.GraphTypePlan - if plan != nil { + if planFile != nil { graphType = terraform.GraphTypeApply } @@ -163,10 +167,10 @@ func (c *GraphCommand) Run(args []string) int { func (c *GraphCommand) Help() string { helpText := ` -Usage: terraform graph [options] [DIR] +Usage: terraform graph [options] Outputs the visual execution graph of Terraform resources according to - configuration files in DIR (or the current directory if omitted). + configuration files in the current directory. The graph is outputted in DOT format. The typical program that can read this format is GraphViz, but many web services are also available diff --git a/command/graph_test.go b/command/graph_test.go index dd9f4413c..bb474696e 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -14,8 +14,10 @@ import ( ) func TestGraph(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("graph"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() ui := new(cli.MockUi) c := &GraphCommand{ @@ -25,9 +27,7 @@ func TestGraph(t *testing.T) { }, } - args := []string{ - testFixturePath("graph"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } @@ -57,14 +57,10 @@ func TestGraph_multipleArgs(t *testing.T) { } func TestGraph_noArgs(t *testing.T) { - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(testFixturePath("graph")); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("graph"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() ui := new(cli.MockUi) c := &GraphCommand{ diff --git a/command/init.go b/command/init.go index 0c663b018..414d80d5a 100644 --- a/command/init.go +++ b/command/init.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "strings" "github.com/hashicorp/hcl/v2" @@ -59,11 +58,11 @@ func (c *InitCommand) Run(args []string) int { c.pluginPath = flagPluginPath } - // Validate the arg count + // Validate the arg count and get the working directory args = cmdFlags.Args() - if len(args) > 1 { - c.Ui.Error("The init command expects at most one argument.\n") - cmdFlags.Usage() + path, err := ModulePath(args) + if err != nil { + c.Ui.Error(err.Error()) return 1 } @@ -72,20 +71,6 @@ func (c *InitCommand) Run(args []string) int { return 1 } - // Get our pwd. We don't always need it but always getting it is easier - // than the logic to determine if it is or isn't needed. - pwd, err := os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - return 1 - } - - // If an argument is provided then it overrides our working directory. - path := pwd - if len(args) == 1 { - path = args[0] - } - // This will track whether we outputted anything so that we know whether // to output a newline before the success message var header bool @@ -924,7 +909,7 @@ func (c *InitCommand) AutocompleteFlags() complete.Flags { func (c *InitCommand) Help() string { helpText := ` -Usage: terraform init [options] [DIR] +Usage: terraform init [options] Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc. @@ -939,9 +924,6 @@ Usage: terraform init [options] [DIR] state. Even so, if you have important information, please back it up prior to running this command, just in case. - If no arguments are given, the configuration in this working directory - is initialized. - Options: -backend=true Configure the backend for this configuration. diff --git a/command/init_test.go b/command/init_test.go index f7f5af923..c72be0456 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -72,42 +72,6 @@ func TestInit_multipleArgs(t *testing.T) { } } -func TestInit_fromModule_explicitDest(t *testing.T) { - td := tempDir(t) - os.MkdirAll(td, 0755) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - } - - if _, err := os.Stat(DefaultStateFilename); err == nil { - // This should never happen; it indicates a bug in another test - // is causing a terraform.tfstate to get left behind in our directory - // here, which can interfere with our init process in a way that - // isn't relevant to this test. - fullPath, _ := filepath.Abs(DefaultStateFilename) - t.Fatalf("some other test has left terraform.tfstate behind:\n%s", fullPath) - } - - args := []string{ - "-from-module=" + testFixturePath("init"), - td, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { - t.Fatalf("err: %s", err) - } -} - func TestInit_fromModule_cwdDest(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) @@ -161,6 +125,10 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { t.Fatalf("err: %s", err) } + if err := os.Chdir("foo"); err != nil { + t.Fatalf("err: %s", err) + } + ui := new(cli.MockUi) c := &InitCommand{ Meta: Meta{ @@ -170,8 +138,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { } args := []string{ - "-from-module=.", - "foo", + "-from-module=./..", } if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) @@ -212,7 +179,7 @@ func TestInit_get(t *testing.T) { func TestInit_getUpgradeModules(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) - os.MkdirAll(td, 0755) + testCopyDir(t, testFixturePath("init-get"), td) defer os.RemoveAll(td) defer testChdir(t, td)() @@ -227,7 +194,6 @@ func TestInit_getUpgradeModules(t *testing.T) { args := []string{ "-get=true", "-upgrade", - testFixturePath("init-get"), } if code := c.Run(args); code != 0 { t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) @@ -484,7 +450,7 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { } output := ui.ErrorWriter.String() - if got, want := output, `Module directory ./input.config does not exist`; !strings.Contains(got, want) { + if got, want := output, `Too many command line arguments`; !strings.Contains(got, want) { t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) } } @@ -681,41 +647,6 @@ func TestInit_backendCli_no_config_block(t *testing.T) { } } -func TestInit_targetSubdir(t *testing.T) { - // Create a temporary working directory that is empty - td := tempDir(t) - os.MkdirAll(td, 0755) - defer os.RemoveAll(td) - defer testChdir(t, td)() - - // copy the source into a subdir - testCopyDir(t, testFixturePath("init-backend"), filepath.Join(td, "source")) - - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), - Ui: ui, - }, - } - - args := []string{ - "source", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - if _, err := os.Stat(filepath.Join(td, DefaultDataDir, DefaultStateFilename)); err != nil { - t.Fatalf("err: %s", err) - } - - // a data directory should not have been added to out working dir - if _, err := os.Stat(filepath.Join(td, "source", DefaultDataDir)); !os.IsNotExist(err) { - t.Fatalf("err: %s", err) - } -} - func TestInit_backendReinitWithExtra(t *testing.T) { td := tempDir(t) testCopyDir(t, testFixturePath("init-backend-empty"), td) @@ -913,11 +844,11 @@ func TestInit_getProvider(t *testing.T) { ui := new(cli.MockUi) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version - "exact": []string{"1.2.3"}, + "exact": {"1.2.3"}, // config requires >= 2.3.3 - "greater-than": []string{"2.3.4", "2.3.3", "2.3.0"}, + "greater-than": {"2.3.4", "2.3.3", "2.3.0"}, // config specifies - "between": []string{"3.4.5", "2.3.4", "1.2.3"}, + "between": {"3.4.5", "2.3.4", "1.2.3"}, }) defer close() m := Meta{ @@ -1015,10 +946,10 @@ func TestInit_getProviderSource(t *testing.T) { ui := new(cli.MockUi) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version - "acme/alpha": []string{"1.2.3"}, + "acme/alpha": {"1.2.3"}, // config doesn't specify versions for other providers - "registry.example.com/acme/beta": []string{"1.0.0"}, - "gamma": []string{"2.0.0"}, + "registry.example.com/acme/beta": {"1.0.0"}, + "gamma": {"2.0.0"}, }) defer close() m := Meta{ @@ -1166,8 +1097,8 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { // unknown provider, and the registry source will allow us to look up the // appropriate namespace if possible. providerSource, psClose := newMockProviderSource(t, map[string][]string{ - "hashicorp/foo": []string{"1.2.3"}, - "terraform-providers/baz": []string{"2.3.4"}, // this will not be installed + "hashicorp/foo": {"1.2.3"}, + "terraform-providers/baz": {"2.3.4"}, // this will not be installed }) defer psClose() registrySource, rsClose := testRegistrySource(t) @@ -1223,15 +1154,14 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { func TestInit_providerSource(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) - configDirName := "init-required-providers" - testCopyDir(t, testFixturePath(configDirName), filepath.Join(td, configDirName)) + testCopyDir(t, testFixturePath("init-required-providers"), td) defer os.RemoveAll(td) defer testChdir(t, td)() providerSource, close := newMockProviderSource(t, map[string][]string{ - "test": []string{"1.2.3", "1.2.4"}, - "test-beta": []string{"1.2.4"}, - "source": []string{"1.2.2", "1.2.3", "1.2.1"}, + "test": {"1.2.3", "1.2.4"}, + "test-beta": {"1.2.4"}, + "source": {"1.2.2", "1.2.3", "1.2.1"}, }) defer close() @@ -1246,7 +1176,7 @@ func TestInit_providerSource(t *testing.T) { Meta: m, } - args := []string{configDirName} + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) @@ -1330,15 +1260,14 @@ func TestInit_cancel(t *testing.T) { // platforms) were sent to it, testing that it is interruptible. td := tempDir(t) - configDirName := "init-required-providers" - testCopyDir(t, testFixturePath(configDirName), filepath.Join(td, configDirName)) + testCopyDir(t, testFixturePath("init-required-providers"), td) defer os.RemoveAll(td) defer testChdir(t, td)() providerSource, closeSrc := newMockProviderSource(t, map[string][]string{ - "test": []string{"1.2.3", "1.2.4"}, - "test-beta": []string{"1.2.4"}, - "source": []string{"1.2.2", "1.2.3", "1.2.1"}, + "test": {"1.2.3", "1.2.4"}, + "test-beta": {"1.2.4"}, + "source": {"1.2.2", "1.2.3", "1.2.1"}, }) defer closeSrc() @@ -1359,7 +1288,7 @@ func TestInit_cancel(t *testing.T) { Meta: m, } - args := []string{configDirName} + args := []string{} if code := c.Run(args); code == 0 { t.Fatalf("succeeded; wanted error") @@ -1382,11 +1311,11 @@ func TestInit_getUpgradePlugins(t *testing.T) { providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version - "exact": []string{"1.2.3"}, + "exact": {"1.2.3"}, // config requires >= 2.3.3 - "greater-than": []string{"2.3.4", "2.3.3", "2.3.0"}, + "greater-than": {"2.3.4", "2.3.3", "2.3.0"}, // config specifies > 1.0.0 , < 3.0.0 - "between": []string{"3.4.5", "2.3.4", "1.2.3"}, + "between": {"3.4.5", "2.3.4", "1.2.3"}, }) defer close() @@ -1398,8 +1327,8 @@ func TestInit_getUpgradePlugins(t *testing.T) { } installFakeProviderPackages(t, &m, map[string][]string{ - "exact": []string{"0.0.1"}, - "greater-than": []string{"2.3.3"}, + "exact": {"0.0.1"}, + "greater-than": {"2.3.3"}, }) c := &InitCommand{ @@ -1506,11 +1435,11 @@ func TestInit_getProviderMissing(t *testing.T) { providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for exact version 1.2.3 - "exact": []string{"1.2.4"}, + "exact": {"1.2.4"}, // config requires >= 2.3.3 - "greater-than": []string{"2.3.4", "2.3.3", "2.3.0"}, + "greater-than": {"2.3.4", "2.3.3", "2.3.0"}, // config specifies - "between": []string{"3.4.5", "2.3.4", "1.2.3"}, + "between": {"3.4.5", "2.3.4", "1.2.3"}, }) defer close() @@ -1716,14 +1645,14 @@ func TestInit_pluginDirProviders(t *testing.T) { // for a moment that they are provider cache directories just because that // allows us to lean on our existing test helper functions to do this. for i, def := range [][]string{ - []string{"exact", "1.2.3"}, - []string{"greater-than", "2.3.4"}, - []string{"between", "2.3.4"}, + {"exact", "1.2.3"}, + {"greater-than", "2.3.4"}, + {"between", "2.3.4"}, } { name, version := def[0], def[1] dir := providercache.NewDir(pluginPath[i]) installFakeProviderPackagesElsewhere(t, dir, map[string][]string{ - name: []string{version}, + name: {version}, }) } @@ -1789,7 +1718,7 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { // but we should ignore it because -plugin-dir is set and thus this // source is temporarily overridden during install. providerSource, close := newMockProviderSource(t, map[string][]string{ - "between": []string{"2.3.4"}, + "between": {"2.3.4"}, }) defer close() @@ -1816,13 +1745,13 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { // for a moment that they are provider cache directories just because that // allows us to lean on our existing test helper functions to do this. for i, def := range [][]string{ - []string{"exact", "1.2.3"}, - []string{"greater-than", "2.3.4"}, + {"exact", "1.2.3"}, + {"greater-than", "2.3.4"}, } { name, version := def[0], def[1] dir := providercache.NewDir(pluginPath[i]) installFakeProviderPackagesElsewhere(t, dir, map[string][]string{ - name: []string{version}, + name: {version}, }) } diff --git a/command/plan.go b/command/plan.go index dc578f42e..7175c0602 100644 --- a/command/plan.go +++ b/command/plan.go @@ -47,21 +47,6 @@ func (c *PlanCommand) Run(args []string) int { return 1 } - // Check if the path is a plan, which is not permitted - planFileReader, err := c.PlanFile(configPath) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - if planFileReader != nil { - c.showDiagnostics(tfdiags.Sourceless( - tfdiags.Error, - "Invalid configuration directory", - fmt.Sprintf("Cannot pass a saved plan file to the 'terraform plan' command. To apply a saved plan, use: terraform apply %s", configPath), - )) - return 1 - } - var diags tfdiags.Diagnostics var backendConfig *configs.Backend @@ -191,7 +176,7 @@ func (c *PlanCommand) Run(args []string) int { func (c *PlanCommand) Help() string { helpText := ` -Usage: terraform plan [options] [DIR] +Usage: terraform plan [options] Generates a speculative execution plan, showing what actions Terraform would take to apply the current configuration. This command will not diff --git a/command/plan_test.go b/command/plan_test.go index 6173f3565..357371fd4 100644 --- a/command/plan_test.go +++ b/command/plan_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "os" + "path" "path/filepath" "strings" "sync" @@ -98,6 +99,11 @@ func TestPlan_plan(t *testing.T) { } func TestPlan_destroy(t *testing.T) { + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ @@ -131,7 +137,6 @@ func TestPlan_destroy(t *testing.T) { "-destroy", "-out", outPath, "-state", statePath, - testFixturePath("plan"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -146,8 +151,10 @@ func TestPlan_destroy(t *testing.T) { } func TestPlan_noState(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() p := planFixtureProvider() ui := new(cli.MockUi) @@ -158,9 +165,7 @@ func TestPlan_noState(t *testing.T) { }, } - args := []string{ - testFixturePath("plan"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } @@ -179,10 +184,11 @@ func TestPlan_noState(t *testing.T) { } func TestPlan_outPath(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() - td := testTempDir(t) outPath := filepath.Join(td, "test.plan") p := planFixtureProvider() @@ -200,7 +206,6 @@ func TestPlan_outPath(t *testing.T) { args := []string{ "-out", outPath, - testFixturePath("plan"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -210,6 +215,11 @@ func TestPlan_outPath(t *testing.T) { } func TestPlan_outPathNoChange(t *testing.T) { + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( addrs.Resource{ @@ -232,7 +242,6 @@ func TestPlan_outPathNoChange(t *testing.T) { }) statePath := testStateFile(t, originalState) - td := testTempDir(t) outPath := filepath.Join(td, "test.plan") p := planFixtureProvider() @@ -247,7 +256,6 @@ func TestPlan_outPathNoChange(t *testing.T) { args := []string{ "-out", outPath, "-state", statePath, - testFixturePath("plan"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -360,8 +368,11 @@ func TestPlan_outBackend(t *testing.T) { } func TestPlan_refreshFalse(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() p := planFixtureProvider() ui := new(cli.MockUi) @@ -374,7 +385,6 @@ func TestPlan_refreshFalse(t *testing.T) { args := []string{ "-refresh=false", - testFixturePath("plan"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -386,6 +396,12 @@ func TestPlan_refreshFalse(t *testing.T) { } func TestPlan_state(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := testState() statePath := testStateFile(t, originalState) @@ -400,7 +416,6 @@ func TestPlan_state(t *testing.T) { args := []string{ "-state", statePath, - testFixturePath("plan"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -422,18 +437,16 @@ func TestPlan_state(t *testing.T) { } func TestPlan_stateDefault(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Generate state and move it to the default path originalState := testState() statePath := testStateFile(t, originalState) - - // Change to that directory - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(filepath.Dir(statePath)); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) + os.Rename(statePath, path.Join(td, "terraform.tfstate")) p := planFixtureProvider() ui := new(cli.MockUi) @@ -444,10 +457,7 @@ func TestPlan_stateDefault(t *testing.T) { }, } - args := []string{ - "-state", statePath, - testFixturePath("plan"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } @@ -514,8 +524,11 @@ func TestPlan_validate(t *testing.T) { } func TestPlan_vars(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() p := planVarsFixtureProvider() ui := new(cli.MockUi) @@ -535,7 +548,6 @@ func TestPlan_vars(t *testing.T) { args := []string{ "-var", "foo=bar", - testFixturePath("plan-vars"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -547,8 +559,11 @@ func TestPlan_vars(t *testing.T) { } func TestPlan_varsUnset(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() // The plan command will prompt for interactive input of var.foo. // We'll answer "bar" to that prompt, which should then allow this @@ -569,9 +584,7 @@ func TestPlan_varsUnset(t *testing.T) { }, } - args := []string{ - testFixturePath("plan-vars"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } @@ -581,8 +594,11 @@ func TestPlan_varsUnset(t *testing.T) { // processing of user input: // https://github.com/hashicorp/terraform/issues/26035 func TestPlan_providerArgumentUnset(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() // Disable test mode so input would be asked test = false @@ -631,17 +647,18 @@ func TestPlan_providerArgumentUnset(t *testing.T) { }, } - args := []string{ - testFixturePath("plan"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } } func TestPlan_varFile(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() varFilePath := testTempFile(t) if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { @@ -666,7 +683,6 @@ func TestPlan_varFile(t *testing.T) { args := []string{ "-var-file", varFilePath, - testFixturePath("plan-vars"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -678,21 +694,17 @@ func TestPlan_varFile(t *testing.T) { } func TestPlan_varFileDefault(t *testing.T) { - varFileDir := testTempDir(t) - varFilePath := filepath.Join(varFileDir, "terraform.tfvars") + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + varFilePath := filepath.Join(td, "terraform.tfvars") if err := ioutil.WriteFile(varFilePath, []byte(planVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(varFileDir); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) - p := planVarsFixtureProvider() ui := new(cli.MockUi) c := &PlanCommand{ @@ -709,9 +721,7 @@ func TestPlan_varFileDefault(t *testing.T) { return } - args := []string{ - testFixturePath("plan-vars"), - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } @@ -722,8 +732,11 @@ func TestPlan_varFileDefault(t *testing.T) { } func TestPlan_varFileWithDecls(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("plan-vars"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() varFilePath := testTempFile(t) if err := ioutil.WriteFile(varFilePath, []byte(planVarFileWithDecl), 0644); err != nil { @@ -741,7 +754,6 @@ func TestPlan_varFileWithDecls(t *testing.T) { args := []string{ "-var-file", varFilePath, - testFixturePath("plan-vars"), } if code := c.Run(args); code == 0 { t.Fatalf("succeeded; want failure\n\n%s", ui.OutputWriter.String()) @@ -796,6 +808,12 @@ func TestPlan_detailedExitcode_emptyDiff(t *testing.T) { } func TestPlan_shutdown(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("apply-shutdown"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + cancelled := make(chan struct{}) shutdownCh := make(chan struct{}) @@ -847,14 +865,7 @@ func TestPlan_shutdown(t *testing.T) { }, } - code := c.Run([]string{ - // Unfortunately it seems like this test can inadvertently pick up - // leftover state from other tests without this. Ideally we should - // find which test is leaving a terraform.tfstate behind and stop it - // doing that, but this will stop this test flapping for now. - "-state=nonexistent.tfstate", - testFixturePath("apply-shutdown"), - }) + code := c.Run([]string{}) if code != 1 { t.Errorf("wrong exit code %d; want 1\noutput:\n%s", code, ui.OutputWriter.String()) } diff --git a/command/refresh.go b/command/refresh.go index a393f0c03..42ed0c4d1 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -104,7 +104,7 @@ func (c *RefreshCommand) Run(args []string) int { func (c *RefreshCommand) Help() string { helpText := ` -Usage: terraform refresh [options] [dir] +Usage: terraform refresh [options] Update the state file of your infrastructure with metadata that matches the physical resources they are tracking. diff --git a/command/refresh_test.go b/command/refresh_test.go index d3d9cd6b1..339290677 100644 --- a/command/refresh_test.go +++ b/command/refresh_test.go @@ -27,6 +27,12 @@ import ( var equateEmpty = cmpopts.EquateEmpty() func TestRefresh(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -49,7 +55,6 @@ func TestRefresh(t *testing.T) { args := []string{ "-state", statePath, - testFixturePath("refresh"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -100,9 +105,7 @@ func TestRefresh_empty(t *testing.T) { }), } - args := []string{ - td, - } + args := []string{} if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } @@ -113,6 +116,12 @@ func TestRefresh_empty(t *testing.T) { } func TestRefresh_lockedState(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -141,7 +150,6 @@ func TestRefresh_lockedState(t *testing.T) { args := []string{ "-state", statePath, - testFixturePath("refresh"), } if code := c.Run(args); code == 0 { @@ -214,6 +222,12 @@ func TestRefresh_cwd(t *testing.T) { } func TestRefresh_defaultState(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + originalState := testState() // Write the state file in a temporary directory with the @@ -258,7 +272,6 @@ func TestRefresh_defaultState(t *testing.T) { args := []string{ "-state", statePath, - testFixturePath("refresh"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -290,6 +303,12 @@ func TestRefresh_defaultState(t *testing.T) { } func TestRefresh_outPath(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -322,7 +341,6 @@ func TestRefresh_outPath(t *testing.T) { args := []string{ "-state", statePath, "-state-out", outPath, - testFixturePath("refresh"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -353,6 +371,12 @@ func TestRefresh_outPath(t *testing.T) { } func TestRefresh_var(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh-var"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -369,7 +393,6 @@ func TestRefresh_var(t *testing.T) { args := []string{ "-var", "foo=bar", "-state", statePath, - testFixturePath("refresh-var"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -384,6 +407,12 @@ func TestRefresh_var(t *testing.T) { } func TestRefresh_varFile(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh-var"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -405,7 +434,6 @@ func TestRefresh_varFile(t *testing.T) { args := []string{ "-var-file", varFilePath, "-state", statePath, - testFixturePath("refresh-var"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -420,6 +448,12 @@ func TestRefresh_varFile(t *testing.T) { } func TestRefresh_varFileDefault(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh-var"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -433,24 +467,13 @@ func TestRefresh_varFileDefault(t *testing.T) { } p.GetSchemaResponse = refreshVarFixtureSchema() - varFileDir := testTempDir(t) - varFilePath := filepath.Join(varFileDir, "terraform.tfvars") + varFilePath := filepath.Join(td, "terraform.tfvars") if err := ioutil.WriteFile(varFilePath, []byte(refreshVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := os.Chdir(varFileDir); err != nil { - t.Fatalf("err: %s", err) - } - defer os.Chdir(cwd) - args := []string{ "-state", statePath, - testFixturePath("refresh-var"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -465,6 +488,12 @@ func TestRefresh_varFileDefault(t *testing.T) { } func TestRefresh_varsUnset(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh-unset-var"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + // Disable test mode so input would be asked test = false defer func() { test = true }() @@ -497,7 +526,6 @@ func TestRefresh_varsUnset(t *testing.T) { args := []string{ "-state", statePath, - testFixturePath("refresh-unset-var"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -505,6 +533,12 @@ func TestRefresh_varsUnset(t *testing.T) { } func TestRefresh_backup(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -553,7 +587,6 @@ func TestRefresh_backup(t *testing.T) { "-state", statePath, "-state-out", outPath, "-backup", backupPath, - testFixturePath("refresh"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -584,6 +617,12 @@ func TestRefresh_backup(t *testing.T) { } func TestRefresh_disableBackup(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -617,7 +656,6 @@ func TestRefresh_disableBackup(t *testing.T) { "-state", statePath, "-state-out", outPath, "-backup", "-", - testFixturePath("refresh"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) @@ -653,6 +691,12 @@ func TestRefresh_disableBackup(t *testing.T) { } func TestRefresh_displaysOutputs(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("refresh-output"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + state := testState() statePath := testStateFile(t, state) @@ -679,7 +723,6 @@ func TestRefresh_displaysOutputs(t *testing.T) { args := []string{ "-state", statePath, - testFixturePath("refresh-output"), } if code := c.Run(args); code != 0 { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) diff --git a/command/unlock.go b/command/unlock.go index 6376fd0f6..fd4b4f36b 100644 --- a/command/unlock.go +++ b/command/unlock.go @@ -30,8 +30,8 @@ func (c *UnlockCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) == 0 { - c.Ui.Error("unlock requires a lock id argument") + if len(args) != 1 { + c.Ui.Error("Expected a single argument: LOCK_ID") return cli.RunResultHelp } @@ -116,7 +116,7 @@ func (c *UnlockCommand) Run(args []string) int { func (c *UnlockCommand) Help() string { helpText := ` -Usage: terraform force-unlock LOCK_ID [DIR] +Usage: terraform force-unlock LOCK_ID Manually unlock the state for the defined configuration. diff --git a/command/unlock_test.go b/command/unlock_test.go index affd0bdbb..70f14c65a 100644 --- a/command/unlock_test.go +++ b/command/unlock_test.go @@ -56,7 +56,7 @@ func TestUnlock(t *testing.T) { "-force", } - if code := c.Run(args); code != 1 { + if code := c.Run(args); code != cli.RunResultHelp { t.Fatalf("bad: %d\n%s\n%s", code, ui.OutputWriter.String(), ui.ErrorWriter.String()) } } diff --git a/command/workspace_delete.go b/command/workspace_delete.go index 522b38e05..578ac77a8 100644 --- a/command/workspace_delete.go +++ b/command/workspace_delete.go @@ -35,8 +35,8 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) == 0 { - c.Ui.Error("expected NAME.\n") + if len(args) != 1 { + c.Ui.Error("Expected a single argument: NAME.\n") return cli.RunResultHelp } @@ -182,7 +182,7 @@ func (c *WorkspaceDeleteCommand) AutocompleteFlags() complete.Flags { func (c *WorkspaceDeleteCommand) Help() string { helpText := ` -Usage: terraform workspace delete [OPTIONS] NAME [DIR] +Usage: terraform workspace delete [OPTIONS] NAME Delete a Terraform workspace diff --git a/command/workspace_list.go b/command/workspace_list.go index 7a5e4f1fd..51c345ed6 100644 --- a/command/workspace_list.go +++ b/command/workspace_list.go @@ -91,7 +91,7 @@ func (c *WorkspaceListCommand) AutocompleteFlags() complete.Flags { func (c *WorkspaceListCommand) Help() string { helpText := ` -Usage: terraform workspace list [DIR] +Usage: terraform workspace list List Terraform workspaces. diff --git a/command/workspace_new.go b/command/workspace_new.go index 40a774559..def5e50a8 100644 --- a/command/workspace_new.go +++ b/command/workspace_new.go @@ -37,7 +37,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) == 0 { + if len(args) != 1 { c.Ui.Error("Expected a single argument: NAME.\n") return cli.RunResultHelp } @@ -176,7 +176,7 @@ func (c *WorkspaceNewCommand) AutocompleteFlags() complete.Flags { func (c *WorkspaceNewCommand) Help() string { helpText := ` -Usage: terraform workspace new [OPTIONS] NAME [DIR] +Usage: terraform workspace new [OPTIONS] NAME Create a new Terraform workspace. diff --git a/command/workspace_select.go b/command/workspace_select.go index b46834c1d..801853bdb 100644 --- a/command/workspace_select.go +++ b/command/workspace_select.go @@ -26,7 +26,7 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { } args = cmdFlags.Args() - if len(args) == 0 { + if len(args) != 1 { c.Ui.Error("Expected a single argument: NAME.\n") return cli.RunResultHelp } @@ -129,7 +129,7 @@ func (c *WorkspaceSelectCommand) AutocompleteFlags() complete.Flags { func (c *WorkspaceSelectCommand) Help() string { helpText := ` -Usage: terraform workspace select NAME [DIR] +Usage: terraform workspace select NAME Select a different Terraform workspace. From 888f36aebb6defd7b903cb24663e65b93109fea1 Mon Sep 17 00:00:00 2001 From: Alisdair McDiarmid Date: Tue, 2 Feb 2021 13:09:30 -0500 Subject: [PATCH 2/2] cli: Remove positional plan argument from graph To make the command arguments easier to understand and extend, we are moving away from positional arguments. This commit changes the graph command to take a `-plan` flag instead of an optional trailing path. --- command/graph.go | 15 +++++++-------- command/graph_test.go | 2 +- website/docs/cli/commands/graph.html.md | 11 +++++++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/command/graph.go b/command/graph.go index 2267c891f..dfc1a2f8e 100644 --- a/command/graph.go +++ b/command/graph.go @@ -23,6 +23,7 @@ func (c *GraphCommand) Run(args []string) int { var graphTypeStr string var moduleDepth int var verbose bool + var planPath string args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("graph") @@ -30,19 +31,14 @@ func (c *GraphCommand) Run(args []string) int { cmdFlags.StringVar(&graphTypeStr, "type", "", "type") cmdFlags.IntVar(&moduleDepth, "module-depth", -1, "module-depth") cmdFlags.BoolVar(&verbose, "verbose", false, "verbose") + cmdFlags.StringVar(&planPath, "plan", "", "plan") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) return 1 } - args = cmdFlags.Args() - var planPath string - if len(args) > 0 { - planPath = args[0] - args = args[1:] - } - configPath, err := ModulePath(args) + configPath, err := ModulePath(cmdFlags.Args()) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -170,7 +166,7 @@ func (c *GraphCommand) Help() string { Usage: terraform graph [options] Outputs the visual execution graph of Terraform resources according to - configuration files in the current directory. + either the current configuration or an execution plan. The graph is outputted in DOT format. The typical program that can read this format is GraphViz, but many web services are also available @@ -184,6 +180,9 @@ Usage: terraform graph [options] Options: + -plan=tfplan Render graph using the specified plan file instead of the + configuration in the current directory. + -draw-cycles Highlight any cycles in the graph with colored edges. This helps when diagnosing cycle errors. diff --git a/command/graph_test.go b/command/graph_test.go index bb474696e..392db1a9c 100644 --- a/command/graph_test.go +++ b/command/graph_test.go @@ -150,7 +150,7 @@ func TestGraph_plan(t *testing.T) { } args := []string{ - planPath, + "-plan", planPath, } if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) diff --git a/website/docs/cli/commands/graph.html.md b/website/docs/cli/commands/graph.html.md index 04c286b03..dea3d441b 100644 --- a/website/docs/cli/commands/graph.html.md +++ b/website/docs/cli/commands/graph.html.md @@ -18,14 +18,14 @@ The output is in the DOT format, which can be used by Usage: `terraform graph [options]` -Outputs the visual dependency graph of Terraform resources represented by the -configuration in the current working directory. +Outputs the visual execution graph of Terraform resources according to +either the current configuration or an execution plan. The graph is outputted in DOT format. The typical program that can read this format is GraphViz, but many web services are also available to read this format. -The -type flag can be used to control the type of graph shown. Terraform +The `-type` flag can be used to control the type of graph shown. Terraform creates different graphs for different operations. See the options below for the list of types supported. The default type is "plan" if a configuration is given, and "apply" if a plan file is passed as an @@ -33,6 +33,9 @@ argument. Options: +* `-plan=tfplan` - Render graph using the specified plan file instead of the + configuration in the current directory. + * `-draw-cycles` - Highlight any cycles in the graph with colored edges. This helps when diagnosing cycle errors. @@ -48,7 +51,7 @@ The output of `terraform graph` is in the DOT format, which can easily be converted to an image by making use of `dot` provided by GraphViz: -```shell +```shellsession $ terraform graph | dot -Tsvg > graph.svg ```