From 60bc7aa05dbd99dd9e1809a4bec7d1f392f5730d Mon Sep 17 00:00:00 2001 From: Chris Arcand Date: Tue, 21 Sep 2021 23:29:02 -0500 Subject: [PATCH 1/2] command: Auto-select single workspace if necessary When initializing a backend, if the currently selected workspace does not exist, the user is prompted to select from the list of workspaces the backend provides. Instead, we should automatically select the only workspace available _if_ that's all that's there. Although with being a nice bit of polish, this enables future improvments with Terraform Cloud in potentially removing the implicit depenency on always using the 'default' workspace when the current configuration is mapped to a single TFC workspace. --- internal/command/meta_backend.go | 14 +++- internal/command/meta_backend_test.go | 79 ++++++++++++++++++- .../.terraform/environment | 1 + .../.terraform/terraform.tfstate | 23 ++++++ .../main.tf | 7 ++ .../terraform.tfstate | 13 +++ .../terraform.tfstate.d/foo/terraform.tfstate | 13 +++ .../.terraform/environment | 1 + .../.terraform/terraform.tfstate | 23 ++++++ .../main.tf | 7 ++ 10 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/environment create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/main.tf create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate.d/foo/terraform.tfstate create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/environment create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/main.tf diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 82ecd89c7..f467579ba 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -196,13 +196,19 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { var list strings.Builder for i, w := range workspaces { if w == workspace { + log.Printf("[TRACE] Meta.selectWorkspace: the currently selected workspace is present in the configured backend (%s)", workspace) return nil } fmt.Fprintf(&list, "%d. %s\n", i+1, w) } - // If the selected workspace doesn't exist, ask the user to select - // a workspace from the list of existing workspaces. + // If the backend only has a single workspace, select that as the current workspace + if len(workspaces) == 1 { + log.Printf("[TRACE] Meta.selectWorkspace: automatically selecting the single workspace provided by the backend (%s)", workspaces[0]) + return m.SetWorkspace(workspaces[0]) + } + + // Otherwise, ask the user to select a workspace from the list of existing workspaces. v, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ Id: "select-workspace", Query: fmt.Sprintf( @@ -220,7 +226,9 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { return fmt.Errorf("Failed to select workspace: input not a valid number") } - return m.SetWorkspace(workspaces[idx-1]) + workspace = workspaces[idx-1] + log.Printf("[TRACE] Meta.selectWorkspace: setting the current workpace according to user selection (%s)", workspace) + return m.SetWorkspace(workspace) } // BackendForPlan is similar to Backend, but uses backend settings that were diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 82dbb0355..be8fec970 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -789,6 +789,84 @@ func TestMetaBackend_reconfigureChange(t *testing.T) { } } +// Initializing a backend which supports workspaces and does *not* have +// the currently selected workspace should prompt the user with a list of +// workspaces to choose from to select a valid one, if more than one workspace +// is available. +func TestMetaBackend_initSelectedWorkspaceDoesNotExist(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-multi"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Setup the meta + m := testMetaBackend(t, nil) + + defer testInputMap(t, map[string]string{ + "select-workspace": "2", + })() + + // Get the backend + _, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + expected := "foo" + actual, err := m.Workspace() + if err != nil { + t.Fatal(err) + } + + if actual != expected { + t.Fatalf("expected selected workspace to be %q, but was %q", expected, actual) + } +} + +// Initializing a backend which supports workspaces and does *not* have the +// currently selected workspace - and which only has a single workspace - should +// automatically select that single workspace. +func TestMetaBackend_initSelectedWorkspaceDoesNotExistAutoSelect(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + testCopyDir(t, testFixturePath("init-backend-selected-workspace-doesnt-exist-single"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Setup the meta + m := testMetaBackend(t, nil) + + // this should not ask for input + m.input = false + + // Assert test precondition: The current selected workspace is "bar" + previousName, err := m.Workspace() + if err != nil { + t.Fatal(err) + } + + if previousName != "bar" { + t.Fatalf("expected test fixture to start with 'bar' as the current selected workspace") + } + + // Get the backend + _, diags := m.Backend(&BackendOpts{Init: true}) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + expected := "default" + actual, err := m.Workspace() + if err != nil { + t.Fatal(err) + } + + if actual != expected { + t.Fatalf("expected selected workspace to be %q, but was %q", expected, actual) + } +} + // Changing a configured backend, copying state func TestMetaBackend_configuredChangeCopy(t *testing.T) { // Create a temporary working directory that is empty @@ -1267,7 +1345,6 @@ func TestMetaBackend_configuredChangeCopy_multiToNoDefaultWithoutDefault(t *test // Ask input defer testInputMap(t, map[string]string{ "backend-migrate-multistate-to-multistate": "yes", - "select-workspace": "1", })() // Setup the meta diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/environment b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/environment new file mode 100644 index 000000000..5716ca598 --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/environment @@ -0,0 +1 @@ +bar diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/terraform.tfstate b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/terraform.tfstate new file mode 100644 index 000000000..19a90cc6b --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/.terraform/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 3, + "serial": 2, + "lineage": "2f3864a6-1d3e-1999-0f84-36cdb61179d3", + "backend": { + "type": "local", + "config": { + "path": null, + "workspace_dir": null + }, + "hash": 666019178 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/main.tf b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/main.tf new file mode 100644 index 000000000..da6f209e1 --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "local" {} +} + +output "foo" { + value = "bar" +} diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate new file mode 100644 index 000000000..47de0a47e --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate @@ -0,0 +1,13 @@ +{ + "version": 4, + "terraform_version": "1.1.0", + "serial": 1, + "lineage": "cc4bb587-aa35-87ad-b3b7-7abdb574f2a1", + "outputs": { + "foo": { + "value": "bar", + "type": "string" + } + }, + "resources": [] +} diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate.d/foo/terraform.tfstate b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate.d/foo/terraform.tfstate new file mode 100644 index 000000000..70021d04a --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-multi/terraform.tfstate.d/foo/terraform.tfstate @@ -0,0 +1,13 @@ +{ + "version": 4, + "terraform_version": "1.1.0", + "serial": 1, + "lineage": "8ad3c77d-51aa-d90a-4f12-176f538b6e8b", + "outputs": { + "foo": { + "value": "bar", + "type": "string" + } + }, + "resources": [] +} diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/environment b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/environment new file mode 100644 index 000000000..5716ca598 --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/environment @@ -0,0 +1 @@ +bar diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/terraform.tfstate b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/terraform.tfstate new file mode 100644 index 000000000..19a90cc6b --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/.terraform/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 3, + "serial": 2, + "lineage": "2f3864a6-1d3e-1999-0f84-36cdb61179d3", + "backend": { + "type": "local", + "config": { + "path": null, + "workspace_dir": null + }, + "hash": 666019178 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/main.tf b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/main.tf new file mode 100644 index 000000000..da6f209e1 --- /dev/null +++ b/internal/command/testdata/init-backend-selected-workspace-doesnt-exist-single/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "local" {} +} + +output "foo" { + value = "bar" +} From 171cdbbf939e12153d93a1c06806a7ad507ced3c Mon Sep 17 00:00:00 2001 From: Chris Arcand Date: Wed, 22 Sep 2021 15:55:56 -0500 Subject: [PATCH 2/2] command: Clean up testInputResponseMap before failing on unused answers If you don't, the unused answers will persist in the package-level var and bleed in to other tests. --- internal/command/command_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/command/command_test.go b/internal/command/command_test.go index d1a43cf65..929bfb439 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -716,12 +716,15 @@ func testInputMap(t *testing.T, answers map[string]string) func() { // Return the cleanup return func() { - if len(testInputResponseMap) > 0 { - t.Fatalf("expected no unused answers provided to command.testInputMap, got: %v", testInputResponseMap) - } + var unusedAnswers = testInputResponseMap + // First, clean up! test = true testInputResponseMap = nil + + if len(unusedAnswers) > 0 { + t.Fatalf("expected no unused answers provided to command.testInputMap, got: %v", unusedAnswers) + } } }