From 5399bb04610b3c12d8bacd8058959fec7b700434 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Wed, 24 Feb 2021 08:39:50 +0100 Subject: [PATCH 1/4] Implement Terraform Workspace deletion. Fixes #583. Signed-off-by: Damien Duportal --- modules/terraform/workspace.go | 46 ++++++++ modules/terraform/workspace_test.go | 174 ++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) diff --git a/modules/terraform/workspace.go b/modules/terraform/workspace.go index cd6998daa..3fc301eaf 100644 --- a/modules/terraform/workspace.go +++ b/modules/terraform/workspace.go @@ -60,3 +60,49 @@ func nameMatchesWorkspace(name string, workspace string) bool { match, _ := regexp.MatchString(fmt.Sprintf("^\\*?\\s*%s$", name), workspace) return match } + +// WorkspaceDelete removes the specified terraform workspace with the given options. +// It returns the name of the current workspace AFTER deletion, and the returned error (that can be nil). +// If the workspace to delete is the current one, then it tries to switch to the "default" workspace. +// Deleting the workspace "default" is not supported. +func WorkspaceDeleteE(t testing.TestingT, options *Options, name string) (string, error) { + currentWorkspace := RunTerraformCommand(t, options, "workspace", "show") + + if name == "default" { + return currentWorkspace, fmt.Errorf("Deleting the workspace 'default' is not supported") + } + + out, err := RunTerraformCommandE(t, options, "workspace", "list") + if err != nil { + return currentWorkspace, err + } + if !isExistingWorkspace(out, name) { + return currentWorkspace, fmt.Errorf("The workspace %q does not exist.", name) + } + + // Switch workspace before deleting if it is the current + if currentWorkspace == name { + currentWorkspace = WorkspaceSelectOrNew(t, options, "default") + } + + // delete workspace + _, err = RunTerraformCommandE(t, options, "workspace", "delete", name) + + return currentWorkspace, err +} + +// WorkspaceDelete removes the specified terraform workspace with the given options. +// It returns the name of the current workspace AFTER deletion. +// If the workspace to delete is the current one, then it tries to switch to the "default" workspace. +// Deleting the workspace "default" is not supported and only return an empty string (to avoid a fatal error). +func WorkspaceDelete(t testing.TestingT, options *Options, name string) string { + if name == "default" { + return name + } + + out, err := WorkspaceDeleteE(t, options, name) + if err != nil { + t.Fatal(err) + } + return out +} diff --git a/modules/terraform/workspace_test.go b/modules/terraform/workspace_test.go index 4d3d05916..cbca18d4b 100644 --- a/modules/terraform/workspace_test.go +++ b/modules/terraform/workspace_test.go @@ -130,3 +130,177 @@ func TestNameMatchesWorkspace(t *testing.T) { assert.Equal(t, testCase.expected, actual, "Name: %q, Workspace: %q", testCase.name, testCase.workspace) } } + +// Please note that this test depends on other functions that should be mocked to be a unit test. +func TestWorkspaceDeleteE(t *testing.T) { + t.Parallel() + + // state describes an expected status when a given testCase begins + type state struct { + workspaces []string + current string + } + + // testCase describes a named test case with a state, args and expcted results + type testCase struct { + name string + initialState state + toDeleteWorkspace string + expectedCurrent string + expectedErrorMessage string + } + + testCases := []testCase{ + { + name: "delete another existing workspace and stay on current", + initialState: state{ + workspaces: []string{"staging", "production"}, + current: "staging", + }, + toDeleteWorkspace: "production", + expectedCurrent: "staging", + expectedErrorMessage: "", + }, + { + name: "delete current workspace and switch to a specified", + initialState: state{ + workspaces: []string{"staging", "production"}, + current: "production", + }, + toDeleteWorkspace: "production", + expectedCurrent: "default", + expectedErrorMessage: "", + }, + { + name: "delete a non existing workspace should trigger an error", + initialState: state{ + workspaces: []string{"staging", "production"}, + current: "staging", + }, + toDeleteWorkspace: "hellothere", + expectedCurrent: "staging", + expectedErrorMessage: "The workspace \"hellothere\" does not exist.", + }, + { + name: "delete the default workspace triggers an error", + initialState: state{ + workspaces: []string{"staging", "production"}, + current: "staging", + }, + toDeleteWorkspace: "default", + expectedCurrent: "staging", + expectedErrorMessage: "Deleting the workspace 'default' is not supported", + }, + } + + for _, tt := range testCases { + testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-workspace", tt.name) + if err != nil { + t.Fatal(err) + } + + options := &Options{ + TerraformDir: testFolder, + } + + // Set up pre-existing environment based on test case description + for _, existingWorkspace := range tt.initialState.workspaces { + _, err = RunTerraformCommandE(t, options, "workspace", "new", existingWorkspace) + if err != nil { + t.Fatal(err) + } + } + // Switch to the specified workspace + _, err = RunTerraformCommandE(t, options, "workspace", "select", tt.initialState.current) + if err != nil { + t.Fatal(err) + } + + // Testing time, wooohoooo + gotResult, gotErr := WorkspaceDeleteE(t, options, tt.toDeleteWorkspace) + + // Check for errors + if tt.expectedErrorMessage != "" { + assert.Error(t, gotErr) + assert.Equal(t, tt.expectedErrorMessage, gotErr.Error()) + } else { + assert.Nil(t, gotErr) + // Check for results + assert.Equal(t, tt.expectedCurrent, gotResult) + assert.False(t, isExistingWorkspace(RunTerraformCommand(t, options, "workspace", "list"), tt.toDeleteWorkspace)) + } + + } +} + +// Please note that this test depends on other functions that should be mocked to be a unit test. +func TestWorkspaceDelete(t *testing.T) { + t.Parallel() + + // state describes an expected status when a given testCase begins + type state struct { + workspaces []string + current string + } + + // testCase describes a named test case with a state, args and expcted results + type testCase struct { + name string + initialState state + toDeleteWorkspace string + expectedCurrent string + } + + testCases := []testCase{ + { + name: "delete another existing workspace and stay on current", + initialState: state{ + workspaces: []string{"staging", "production"}, + current: "staging", + }, + toDeleteWorkspace: "production", + expectedCurrent: "staging", + }, + { + name: "delete current workspace and switch to a specified", + initialState: state{ + workspaces: []string{"staging", "production"}, + current: "production", + }, + toDeleteWorkspace: "production", + expectedCurrent: "default", + }, + } + + for _, tt := range testCases { + testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-workspace", tt.name) + if err != nil { + t.Fatal(err) + } + + options := &Options{ + TerraformDir: testFolder, + } + + // Set up pre-existing environment based on test case description + for _, existingWorkspace := range tt.initialState.workspaces { + _, err = RunTerraformCommandE(t, options, "workspace", "new", existingWorkspace) + if err != nil { + t.Fatal(err) + } + } + // Switch to the specified workspace + _, err = RunTerraformCommandE(t, options, "workspace", "select", tt.initialState.current) + if err != nil { + t.Fatal(err) + } + + // Testing time, wooohoooo + gotResult := WorkspaceDelete(t, options, tt.toDeleteWorkspace) + + // Check for results + assert.Equal(t, tt.expectedCurrent, gotResult) + assert.False(t, isExistingWorkspace(RunTerraformCommand(t, options, "workspace", "list"), tt.toDeleteWorkspace)) + + } +} From c684c68b4bb9b087a59b1b9496048b322eb132b5 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Wed, 10 Mar 2021 18:36:54 +0100 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Yevgeniy Brikman --- modules/terraform/errors.go | 14 +++ modules/terraform/workspace.go | 19 ++-- modules/terraform/workspace_test.go | 171 ++++++++-------------------- 3 files changed, 74 insertions(+), 130 deletions(-) diff --git a/modules/terraform/errors.go b/modules/terraform/errors.go index d80a8514b..ae0e9a484 100644 --- a/modules/terraform/errors.go +++ b/modules/terraform/errors.go @@ -87,3 +87,17 @@ type PanicWhileParsingVarFile struct { func (err PanicWhileParsingVarFile) Error() string { return fmt.Sprintf("Recovering panic while parsing '%s'. Got error of type '%v': %v", err.ConfigFile, reflect.TypeOf(err.RecoveredValue), err.RecoveredValue) } + +// UnsupportedDefaultWorkspaceDeletion is returned when user tries to delete the workspace "default" +type UnsupportedDefaultWorkspaceDeletion struct{} + +func (err *UnsupportedDefaultWorkspaceDeletion) Error() string { + return "Deleting the workspace 'default' is not supported" +} + +// WorkspaceDoesNotExist is returned when user tries to delete a workspace which does not exist +type WorkspaceDoesNotExist string + +func (err WorkspaceDoesNotExist) Error() string { + return fmt.Sprintf("The workspace %q does not exist.", string(err)) +} diff --git a/modules/terraform/workspace.go b/modules/terraform/workspace.go index 3fc301eaf..92e95dfb5 100644 --- a/modules/terraform/workspace.go +++ b/modules/terraform/workspace.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/gruntwork-io/terratest/modules/testing" + "github.com/stretchr/testify/require" ) // WorkspaceSelectOrNew runs terraform workspace with the given options and the workspace name @@ -66,10 +67,13 @@ func nameMatchesWorkspace(name string, workspace string) bool { // If the workspace to delete is the current one, then it tries to switch to the "default" workspace. // Deleting the workspace "default" is not supported. func WorkspaceDeleteE(t testing.TestingT, options *Options, name string) (string, error) { - currentWorkspace := RunTerraformCommand(t, options, "workspace", "show") + currentWorkspace, err := RunTerraformCommandE(t, options, "workspace", "show") + if err != nil { + return currentWorkspace, err + } if name == "default" { - return currentWorkspace, fmt.Errorf("Deleting the workspace 'default' is not supported") + return currentWorkspace, &UnsupportedDefaultWorkspaceDeletion{} } out, err := RunTerraformCommandE(t, options, "workspace", "list") @@ -77,12 +81,15 @@ func WorkspaceDeleteE(t testing.TestingT, options *Options, name string) (string return currentWorkspace, err } if !isExistingWorkspace(out, name) { - return currentWorkspace, fmt.Errorf("The workspace %q does not exist.", name) + return currentWorkspace, WorkspaceDoesNotExist(name) } // Switch workspace before deleting if it is the current if currentWorkspace == name { - currentWorkspace = WorkspaceSelectOrNew(t, options, "default") + currentWorkspace, err = WorkspaceSelectOrNewE(t, options, "default") + if err != nil { + return currentWorkspace, err + } } // delete workspace @@ -101,8 +108,6 @@ func WorkspaceDelete(t testing.TestingT, options *Options, name string) string { } out, err := WorkspaceDeleteE(t, options, name) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return out } diff --git a/modules/terraform/workspace_test.go b/modules/terraform/workspace_test.go index cbca18d4b..b7ba38b7a 100644 --- a/modules/terraform/workspace_test.go +++ b/modules/terraform/workspace_test.go @@ -5,6 +5,7 @@ import ( "github.com/gruntwork-io/terratest/modules/files" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWorkspaceNew(t *testing.T) { @@ -131,7 +132,6 @@ func TestNameMatchesWorkspace(t *testing.T) { } } -// Please note that this test depends on other functions that should be mocked to be a unit test. func TestWorkspaceDeleteE(t *testing.T) { t.Parallel() @@ -143,11 +143,11 @@ func TestWorkspaceDeleteE(t *testing.T) { // testCase describes a named test case with a state, args and expcted results type testCase struct { - name string - initialState state - toDeleteWorkspace string - expectedCurrent string - expectedErrorMessage string + name string + initialState state + toDeleteWorkspace string + expectedCurrent string + expectedError error } testCases := []testCase{ @@ -157,9 +157,9 @@ func TestWorkspaceDeleteE(t *testing.T) { workspaces: []string{"staging", "production"}, current: "staging", }, - toDeleteWorkspace: "production", - expectedCurrent: "staging", - expectedErrorMessage: "", + toDeleteWorkspace: "production", + expectedCurrent: "staging", + expectedError: nil, }, { name: "delete current workspace and switch to a specified", @@ -167,9 +167,9 @@ func TestWorkspaceDeleteE(t *testing.T) { workspaces: []string{"staging", "production"}, current: "production", }, - toDeleteWorkspace: "production", - expectedCurrent: "default", - expectedErrorMessage: "", + toDeleteWorkspace: "production", + expectedCurrent: "default", + expectedError: nil, }, { name: "delete a non existing workspace should trigger an error", @@ -177,9 +177,9 @@ func TestWorkspaceDeleteE(t *testing.T) { workspaces: []string{"staging", "production"}, current: "staging", }, - toDeleteWorkspace: "hellothere", - expectedCurrent: "staging", - expectedErrorMessage: "The workspace \"hellothere\" does not exist.", + toDeleteWorkspace: "hellothere", + expectedCurrent: "staging", + expectedError: WorkspaceDoesNotExist("hellothere"), }, { name: "delete the default workspace triggers an error", @@ -187,120 +187,45 @@ func TestWorkspaceDeleteE(t *testing.T) { workspaces: []string{"staging", "production"}, current: "staging", }, - toDeleteWorkspace: "default", - expectedCurrent: "staging", - expectedErrorMessage: "Deleting the workspace 'default' is not supported", + toDeleteWorkspace: "default", + expectedCurrent: "staging", + expectedError: &UnsupportedDefaultWorkspaceDeletion{}, }, } - for _, tt := range testCases { - testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-workspace", tt.name) - if err != nil { - t.Fatal(err) - } - - options := &Options{ - TerraformDir: testFolder, - } - - // Set up pre-existing environment based on test case description - for _, existingWorkspace := range tt.initialState.workspaces { - _, err = RunTerraformCommandE(t, options, "workspace", "new", existingWorkspace) - if err != nil { - t.Fatal(err) + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-workspace", testCase.name) + require.NoError(t, err) + + options := &Options{ + TerraformDir: testFolder, } - } - // Switch to the specified workspace - _, err = RunTerraformCommandE(t, options, "workspace", "select", tt.initialState.current) - if err != nil { - t.Fatal(err) - } - - // Testing time, wooohoooo - gotResult, gotErr := WorkspaceDeleteE(t, options, tt.toDeleteWorkspace) - - // Check for errors - if tt.expectedErrorMessage != "" { - assert.Error(t, gotErr) - assert.Equal(t, tt.expectedErrorMessage, gotErr.Error()) - } else { - assert.Nil(t, gotErr) - // Check for results - assert.Equal(t, tt.expectedCurrent, gotResult) - assert.False(t, isExistingWorkspace(RunTerraformCommand(t, options, "workspace", "list"), tt.toDeleteWorkspace)) - } - - } -} - -// Please note that this test depends on other functions that should be mocked to be a unit test. -func TestWorkspaceDelete(t *testing.T) { - t.Parallel() - - // state describes an expected status when a given testCase begins - type state struct { - workspaces []string - current string - } - - // testCase describes a named test case with a state, args and expcted results - type testCase struct { - name string - initialState state - toDeleteWorkspace string - expectedCurrent string - } - - testCases := []testCase{ - { - name: "delete another existing workspace and stay on current", - initialState: state{ - workspaces: []string{"staging", "production"}, - current: "staging", - }, - toDeleteWorkspace: "production", - expectedCurrent: "staging", - }, - { - name: "delete current workspace and switch to a specified", - initialState: state{ - workspaces: []string{"staging", "production"}, - current: "production", - }, - toDeleteWorkspace: "production", - expectedCurrent: "default", - }, - } - for _, tt := range testCases { - testFolder, err := files.CopyTerraformFolderToTemp("../../test/fixtures/terraform-workspace", tt.name) - if err != nil { - t.Fatal(err) - } - - options := &Options{ - TerraformDir: testFolder, - } - - // Set up pre-existing environment based on test case description - for _, existingWorkspace := range tt.initialState.workspaces { - _, err = RunTerraformCommandE(t, options, "workspace", "new", existingWorkspace) - if err != nil { - t.Fatal(err) + // Set up pre-existing environment based on test case description + for _, existingWorkspace := range testCase.initialState.workspaces { + _, err = RunTerraformCommandE(t, options, "workspace", "new", existingWorkspace) + require.NoError(t, err) + } + // Switch to the specified workspace + _, err = RunTerraformCommandE(t, options, "workspace", "select", testCase.initialState.current) + require.NoError(t, err) + + // Testing time, wooohoooo + gotResult, gotErr := WorkspaceDeleteE(t, options, testCase.toDeleteWorkspace) + + // Check for errors + if testCase.expectedError != nil { + assert.EqualError(t, gotErr, testCase.expectedError.Error()) + } else { + assert.NoError(t, gotErr) + // Check for results + assert.Equal(t, testCase.expectedCurrent, gotResult) + assert.False(t, isExistingWorkspace(RunTerraformCommand(t, options, "workspace", "list"), testCase.toDeleteWorkspace)) } - } - // Switch to the specified workspace - _, err = RunTerraformCommandE(t, options, "workspace", "select", tt.initialState.current) - if err != nil { - t.Fatal(err) - } - - // Testing time, wooohoooo - gotResult := WorkspaceDelete(t, options, tt.toDeleteWorkspace) - - // Check for results - assert.Equal(t, tt.expectedCurrent, gotResult) - assert.False(t, isExistingWorkspace(RunTerraformCommand(t, options, "workspace", "list"), tt.toDeleteWorkspace)) + }) } } From 6f6a2bdb2fa6ce1d2109ee16a922d0b2e5a9b452 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Tue, 23 Mar 2021 11:44:59 +0100 Subject: [PATCH 3/4] Code review Signed-off-by: Damien Duportal --- modules/terraform/workspace.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/terraform/workspace.go b/modules/terraform/workspace.go index 92e95dfb5..1778d33a0 100644 --- a/modules/terraform/workspace.go +++ b/modules/terraform/workspace.go @@ -103,10 +103,6 @@ func WorkspaceDeleteE(t testing.TestingT, options *Options, name string) (string // If the workspace to delete is the current one, then it tries to switch to the "default" workspace. // Deleting the workspace "default" is not supported and only return an empty string (to avoid a fatal error). func WorkspaceDelete(t testing.TestingT, options *Options, name string) string { - if name == "default" { - return name - } - out, err := WorkspaceDeleteE(t, options, name) require.NoError(t, err) return out From 976edceed56c50f101142061243e42ea85fc0f48 Mon Sep 17 00:00:00 2001 From: Damien Duportal Date: Tue, 23 Mar 2021 11:50:54 +0100 Subject: [PATCH 4/4] Code review Signed-off-by: Damien Duportal --- modules/terraform/workspace_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/terraform/workspace_test.go b/modules/terraform/workspace_test.go index b7ba38b7a..0ccb3e752 100644 --- a/modules/terraform/workspace_test.go +++ b/modules/terraform/workspace_test.go @@ -1,6 +1,7 @@ package terraform import ( + "errors" "testing" "github.com/gruntwork-io/terratest/modules/files" @@ -218,7 +219,7 @@ func TestWorkspaceDeleteE(t *testing.T) { // Check for errors if testCase.expectedError != nil { - assert.EqualError(t, gotErr, testCase.expectedError.Error()) + assert.True(t, errors.As(gotErr, &testCase.expectedError)) } else { assert.NoError(t, gotErr) // Check for results