diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go index e52d18325e..94df38df07 100644 --- a/internal/command/arguments/init.go +++ b/internal/command/arguments/init.go @@ -79,10 +79,6 @@ type Init struct { // TODO(SarahFrench/radeksimko): Remove this once the feature is no longer // experimental EnablePssExperiment bool - - // CreateDefaultWorkspace indicates whether the default workspace should be created by - // Terraform when initializing a state store for the first time. - CreateDefaultWorkspace bool } // ParseInit processes CLI arguments, returning an Init value and errors. @@ -116,7 +112,6 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti cmdFlags.BoolVar(&init.Json, "json", false, "json") cmdFlags.Var(&init.BackendConfig, "backend-config", "") cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory") - cmdFlags.BoolVar(&init.CreateDefaultWorkspace, "create-default-workspace", true, "when -input=false, use this flag to block creation of the default workspace") // Used for enabling experimental code that's invoked before configuration is parsed. cmdFlags.BoolVar(&init.EnablePssExperiment, "enable-pluggable-state-storage-experiment", false, "Enable the pluggable state storage experiment") @@ -133,13 +128,6 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti init.EnablePssExperiment = true } - if v := os.Getenv("TF_SKIP_CREATE_DEFAULT_WORKSPACE"); v != "" { - // If TF_SKIP_CREATE_DEFAULT_WORKSPACE is set it will override - // a -create-default-workspace=true flag that's set explicitly, - // as that's indistinguishable from the default value being used. - init.CreateDefaultWorkspace = false - } - if !experimentsEnabled { // If experiments aren't enabled then these flags should not be used. if init.EnablePssExperiment { @@ -149,24 +137,6 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti "Terraform cannot use the -enable-pluggable-state-storage-experiment flag (or TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable) unless experiments are enabled.", )) } - if !init.CreateDefaultWorkspace { - // Can only be set to false by using the flag - // and we cannot identify if -create-default-workspace=true is set explicitly. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Cannot use -create-default-workspace flag without experiments enabled", - "Terraform cannot use the -create-default-workspace flag (or TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable) unless experiments are enabled.", - )) - } - } else { - // Errors using flags despite experiments being enabled. - if !init.CreateDefaultWorkspace && !init.EnablePssExperiment { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled", - "Terraform cannot use the -create-default-workspace=false flag (or TF_SKIP_CREATE_DEFAULT_WORKSPACE environment variable) unless you also supply the -enable-pluggable-state-storage-experiment flag (or set the TF_ENABLE_PLUGGABLE_STATE_STORAGE environment variable).", - )) - } } if init.MigrateState && init.Json { diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index e6df4a6faa..7fdc96fb9e 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -40,19 +40,20 @@ func TestParseInit_basicValid(t *testing.T) { FlagName: "-backend-config", Items: &flagNameValue, }, - Vars: &Vars{}, - InputEnabled: true, - CompactWarnings: false, - TargetFlags: nil, - CreateDefaultWorkspace: true, + Vars: &Vars{}, + InputEnabled: true, + CompactWarnings: false, + TargetFlags: nil, }, }, "setting multiple options": { - []string{"-backend=false", "-force-copy=true", + []string{ + "-backend=false", "-force-copy=true", "-from-module=./main-dir", "-json", "-get=false", "-lock=false", "-lock-timeout=10s", "-reconfigure=true", "-upgrade=true", "-lockfile=readonly", "-compact-warnings=true", - "-ignore-remote-version=true", "-test-directory=./test-dir"}, + "-ignore-remote-version=true", "-test-directory=./test-dir", + }, &Init{ FromModule: "./main-dir", Lockfile: "readonly", @@ -73,12 +74,11 @@ func TestParseInit_basicValid(t *testing.T) { FlagName: "-backend-config", Items: &flagNameValue, }, - Vars: &Vars{}, - InputEnabled: true, - Args: []string{}, - CompactWarnings: true, - TargetFlags: nil, - CreateDefaultWorkspace: true, + Vars: &Vars{}, + InputEnabled: true, + Args: []string{}, + CompactWarnings: true, + TargetFlags: nil, }, }, "with cloud option": { @@ -103,12 +103,11 @@ func TestParseInit_basicValid(t *testing.T) { FlagName: "-backend-config", Items: &[]FlagNameValue{{Name: "-backend-config", Value: "backend.config"}}, }, - Vars: &Vars{}, - InputEnabled: false, - Args: []string{}, - CompactWarnings: false, - TargetFlags: []string{"foo_bar.baz"}, - CreateDefaultWorkspace: true, + Vars: &Vars{}, + InputEnabled: false, + Args: []string{}, + CompactWarnings: false, + TargetFlags: []string{"foo_bar.baz"}, }, }, } @@ -194,30 +193,6 @@ func TestParseInit_experimentalFlags(t *testing.T) { experimentsEnabled: false, wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled", }, - "error: -create-default-workspace=false and experiments are disabled": { - args: []string{"-create-default-workspace=false"}, - experimentsEnabled: false, - wantErr: "Cannot use -create-default-workspace flag without experiments enabled", - }, - "error: TF_SKIP_CREATE_DEFAULT_WORKSPACE is set and experiments are disabled": { - envs: map[string]string{ - "TF_SKIP_CREATE_DEFAULT_WORKSPACE": "1", - }, - experimentsEnabled: false, - wantErr: "Cannot use -create-default-workspace flag without experiments enabled", - }, - "error: -create-default-workspace=false used without -enable-pluggable-state-storage-experiment, while experiments are enabled": { - args: []string{"-create-default-workspace=false"}, - experimentsEnabled: true, - wantErr: "Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled", - }, - "error: TF_SKIP_CREATE_DEFAULT_WORKSPACE used without -enable-pluggable-state-storage-experiment, while experiments are enabled": { - envs: map[string]string{ - "TF_SKIP_CREATE_DEFAULT_WORKSPACE": "1", - }, - experimentsEnabled: true, - wantErr: "Cannot use -create-default-workspace=false flag unless the pluggable state storage experiment is enabled", - }, } for name, tc := range testCases { diff --git a/internal/command/e2etest/pluggable_state_store_test.go b/internal/command/e2etest/pluggable_state_store_test.go index 66865804c7..208a8001fb 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "os" @@ -114,9 +115,6 @@ func TestPrimary_stateStore_unmanaged_separatePlan(t *testing.T) { if !provider.ReadStateBytesCalled() { t.Error("ReadStateBytes not called on un-managed provider") } - if !provider.WriteStateBytesCalled() { - t.Error("WriteStateBytes not called on un-managed provider") - } provider.ResetReadStateBytesCalled() provider.ResetWriteStateBytesCalled() @@ -211,12 +209,9 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } - fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate")) - if err != nil { - t.Fatalf("failed to open default workspace's state file: %s", err) - } - if fi.Size() == 0 { - t.Fatal("default workspace's state file should not have size 0 bytes") + _, err = os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate")) + if !errors.Is(err, os.ErrNotExist) { + t.Fatal("expected default workspace's state file to not exist, but it exists") } //// Create Workspace: terraform workspace new @@ -229,7 +224,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { if !strings.Contains(stdout, expectedMsg) { t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) } - fi, err = os.Stat(path.Join(tf.WorkDir(), workspaceDirName, newWorkspace, "terraform.tfstate")) + fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, newWorkspace, "terraform.tfstate")) if err != nil { t.Fatalf("failed to open %s workspace's state file: %s", newWorkspace, err) } @@ -248,13 +243,13 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { //// Select Workspace: terraform workspace select selectedWorkspace := "default" - stdout, stderr, err = tf.Run("workspace", "select", selectedWorkspace, "-no-color") + stdout, stderr, err = tf.Run("workspace", "select", "-or-create", selectedWorkspace, "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } - expectedMsg = fmt.Sprintf("Switched to workspace %q.", selectedWorkspace) + expectedMsg = fmt.Sprintf("Created and switched to workspace %q!", selectedWorkspace) if !strings.Contains(stdout, expectedMsg) { - t.Errorf("unexpected output, expected %q, but got:\n%s", expectedMsg, stdout) + t.Errorf("unexpected output, expected %s, but got:\n%s", expectedMsg, stdout) } //// Show Workspace: terraform workspace show @@ -640,13 +635,7 @@ func TestPrimary_stateStore_providerCmds(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } - fi, err := os.Stat(path.Join(tf.WorkDir(), workspaceDirName, "default", "terraform.tfstate")) - if err != nil { - t.Fatalf("failed to open default workspace's state file: %s", err) - } - if fi.Size() == 0 { - t.Fatal("default workspace's state file should not have size 0 bytes") - } + // Note: The default state was already created earlier in the test //// Providers: `terraform providers` stdout, stderr, err := tf.Run("providers", "-no-color") diff --git a/internal/command/e2etest/primary_test.go b/internal/command/e2etest/primary_test.go index 583691c110..6720305189 100644 --- a/internal/command/e2etest/primary_test.go +++ b/internal/command/e2etest/primary_test.go @@ -146,7 +146,6 @@ func TestPrimarySeparatePlan(t *testing.T) { if len(stateResources) != 0 { t.Errorf("wrong resources in state after destroy; want none, but still have:%s", spew.Sdump(stateResources)) } - } func TestPrimaryChdirOption(t *testing.T) { @@ -236,7 +235,6 @@ func TestPrimaryChdirOption(t *testing.T) { } func TestPrimary_stateStore(t *testing.T) { - if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build @@ -270,20 +268,16 @@ func TestPrimary_stateStore(t *testing.T) { } //// INIT - stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") + _, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) } - if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") { - t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout) - } - //// PLAN // No separate plan step; this test lets the apply make a plan. //// APPLY - stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color") + stdout, stderr, err := tf.Run("apply", "-auto-approve", "-no-color") if err != nil { t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) } @@ -315,7 +309,6 @@ func TestPrimary_stateStore(t *testing.T) { } func TestPrimary_stateStore_planFile(t *testing.T) { - if !canRunGoBuild { // We're running in a separate-build-then-run context, so we can't // currently execute this test which depends on being able to build @@ -348,15 +341,11 @@ func TestPrimary_stateStore_planFile(t *testing.T) { } //// INIT - stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") + _, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) } - if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") { - t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout) - } - //// PLAN planFile := "testplan" _, stderr, err = tf.Run("plan", "-out="+planFile, "-no-color") @@ -365,7 +354,7 @@ func TestPrimary_stateStore_planFile(t *testing.T) { } //// APPLY - stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color", planFile) + stdout, stderr, err := tf.Run("apply", "-auto-approve", "-no-color", planFile) if err != nil { t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) } @@ -432,15 +421,11 @@ func TestPrimary_stateStore_inMem(t *testing.T) { // // Note - the inmem PSS implementation means that the default workspace state created during init // is lost as soon as the command completes. - stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") + _, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) } - if !strings.Contains(stdout, "Terraform created an empty state file for the default workspace") { - t.Errorf("notice about creating the default workspace is missing from init output:\n%s", stdout) - } - //// PLAN // No separate plan step; this test lets the apply make a plan. @@ -448,7 +433,7 @@ func TestPrimary_stateStore_inMem(t *testing.T) { // // Note - the inmem PSS implementation means that writing to the default workspace during apply // is creating the default state file for the first time. - stdout, stderr, err = tf.Run("apply", "-auto-approve", "-no-color") + stdout, stderr, err := tf.Run("apply", "-auto-approve", "-no-color") if err != nil { t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) } diff --git a/internal/command/init.go b/internal/command/init.go index 6bcb08c5ff..7033efba00 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -232,12 +232,11 @@ func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, ini } opts = &BackendOpts{ - StateStoreConfig: root.StateStore, - Locks: configLocks, - CreateDefaultWorkspace: initArgs.CreateDefaultWorkspace, - ConfigOverride: configOverride, - Init: true, - ViewType: initArgs.ViewType, + StateStoreConfig: root.StateStore, + Locks: configLocks, + ConfigOverride: configOverride, + Init: true, + ViewType: initArgs.ViewType, } case root.Backend != nil: @@ -561,7 +560,6 @@ func (c *InitCommand) getProvidersFromState(ctx context.Context, state *states.S // The calling code is expected to provide the previous locks (if any) and the two sets of locks determined from // configuration and state data. func (c *InitCommand) saveDependencyLockFile(previousLocks, configLocks, stateLocks *depsfile.Locks, flagLockfile string, view views.Init) (output bool, diags tfdiags.Diagnostics) { - // Get the combination of config and state locks newLocks := c.mergeLockedDependencies(configLocks, stateLocks) @@ -631,7 +629,6 @@ func (c *InitCommand) saveDependencyLockFile(previousLocks, configLocks, stateLo // when a specific type of event occurs during provider installation. // The calling code needs to provide a tfdiags.Diagnostics collection, so that provider installation code returns diags to the calling code using closures func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerreqs.Requirements, diags *tfdiags.Diagnostics, inst *providercache.Installer, view views.Init, initMsg views.InitMessageCode, reuseMsg views.InitMessageCode) *providercache.InstallerEvents { - // Because we're currently just streaming a series of events sequentially // into the terminal, we're showing only a subset of the events to keep // things relatively concise. Later it'd be nice to have a progress UI @@ -758,7 +755,6 @@ func (c *InitCommand) prepareInstallerEvents(ctx context.Context, reqs providerr ), )) } - }, QueryPackagesWarning: func(provider addrs.Provider, warnings []string) { displayWarnings := make([]string, len(warnings)) @@ -1151,13 +1147,6 @@ Options: -enable-pluggable-state-storage-experiment [EXPERIMENTAL] A flag to enable an alternative init command that allows use of pluggable state storage. Only usable with experiments enabled. - - -create-default-workspace [EXPERIMENTAL] - This flag must be used alongside the -enable-pluggable-state-storage- - experiment flag with experiments enabled. This flag's value defaults - to true, which allows the default workspace to be created if it does - not exist. Use -create-default-workspace=false to disable this behavior. - ` return strings.TrimSpace(helpText) } diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 5216dcfb1e..bda5709cca 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3494,7 +3494,6 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { output := testOutput.All() expectedOutputs := []string{ "Initializing the state store...", - "Terraform created an empty state file for the default workspace", "Terraform has been successfully initialized!", } for _, expected := range expectedOutputs { @@ -3504,7 +3503,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { } }) - t.Run("the init command creates a backend state file, and creates the default workspace by default", func(t *testing.T) { + t.Run("the init command creates a backend state file, and the default workspace is not made by default", func(t *testing.T) { // Create a temporary, uninitialized working directory with configuration including a state store td := t.TempDir() testCopyDir(t, testFixturePath("init-with-state-store"), td) @@ -3547,7 +3546,6 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { output := testOutput.All() expectedOutputs := []string{ "Initializing the state store...", - "Terraform created an empty state file for the default workspace", "Terraform has been successfully initialized!", } for _, expected := range expectedOutputs { @@ -3556,9 +3554,9 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { } } - // Assert the default workspace was created - if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists { - t.Fatal("expected the default workspace to be created during init, but it is missing") + // Assert the default workspace was not created + if _, exists := mockProvider.MockStates[backend.DefaultStateName]; exists { + t.Fatal("expected the default workspace to not be created during init, but it exists") } // Assert contents of the backend state file @@ -3591,105 +3589,6 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { } }) - t.Run("an init command with the flag -create-default-workspace=false will not make the default workspace by default", func(t *testing.T) { - // Create a temporary, uninitialized working directory with configuration including a state store - td := t.TempDir() - testCopyDir(t, testFixturePath("init-with-state-store"), td) - t.Chdir(td) - - mockProvider := mockPluggableStateStorageProvider() - mockProviderAddress := addrs.NewDefaultProvider("test") - providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, - }) - defer close() - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - Ui: ui, - View: view, - AllowExperimentalFeatures: true, - testingOverrides: &testingOverrides{ - Providers: map[addrs.Provider]providers.Factory{ - mockProviderAddress: providers.FactoryFixed(mockProvider), - }, - }, - ProviderSource: providerSource, - }, - } - - args := []string{"-enable-pluggable-state-storage-experiment=true", "-create-default-workspace=false"} - code := c.Run(args) - testOutput := done(t) - if code != 0 { - t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, testOutput.All()) - } - - // Check output - output := testOutput.All() - expectedOutput := `Terraform has been configured to skip creation of the default workspace` - if !strings.Contains(output, expectedOutput) { - t.Fatalf("expected output to include %q, but got':\n %s", expectedOutput, output) - } - - // Assert the default workspace was created - if _, exists := mockProvider.MockStates[backend.DefaultStateName]; exists { - t.Fatal("expected Terraform to skip creating the default workspace, but it has been created") - } - }) - - t.Run("an init command with TF_SKIP_CREATE_DEFAULT_WORKSPACE set will not make the default workspace by default", func(t *testing.T) { - // Create a temporary, uninitialized working directory with configuration including a state store - td := t.TempDir() - testCopyDir(t, testFixturePath("init-with-state-store"), td) - t.Chdir(td) - - mockProvider := mockPluggableStateStorageProvider() - mockProviderAddress := addrs.NewDefaultProvider("test") - providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, - }) - defer close() - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - Ui: ui, - View: view, - AllowExperimentalFeatures: true, - testingOverrides: &testingOverrides{ - Providers: map[addrs.Provider]providers.Factory{ - mockProviderAddress: providers.FactoryFixed(mockProvider), - }, - }, - ProviderSource: providerSource, - }, - } - - t.Setenv("TF_SKIP_CREATE_DEFAULT_WORKSPACE", "1") // any value - args := []string{"-enable-pluggable-state-storage-experiment=true"} - code := c.Run(args) - testOutput := done(t) - if code != 0 { - t.Fatalf("expected code 0 exit code, got %d, output: \n%s", code, testOutput.All()) - } - - // Check output - output := testOutput.All() - expectedOutput := `Terraform has been configured to skip creation of the default workspace` - if !strings.Contains(output, expectedOutput) { - t.Fatalf("expected output to include %q, but got':\n %s", expectedOutput, output) - } - - // Assert the default workspace was created - if _, exists := mockProvider.MockStates[backend.DefaultStateName]; exists { - t.Fatal("expected Terraform to skip creating the default workspace, but it has been created") - } - }) - // This scenario would be rare, but protecting against it is easy and avoids assumptions. t.Run("if a custom workspace is selected but no workspaces exist an error is returned", func(t *testing.T) { // Create a temporary, uninitialized working directory with configuration including a state store diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index f055188088..710558b800 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -44,7 +44,6 @@ import ( "github.com/hashicorp/terraform/internal/getproviders/reattach" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -83,10 +82,6 @@ type BackendOpts struct { // ViewType will set console output format for the // initialization operation (JSON or human-readable). ViewType arguments.ViewType - - // CreateDefaultWorkspace signifies whether the operations backend should create - // the default workspace or not - CreateDefaultWorkspace bool } // BackendWithRemoteTerraformVersion is a shared interface between the 'remote' and 'cloud' backends @@ -1259,8 +1254,32 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // Verify that selected workspace exist. Otherwise prompt user to create one if opts.Init && savedStateStore != nil { if err := m.selectWorkspace(savedStateStore); err != nil { - diags = diags.Append(err) - return nil, diags + if errors.Is(err, &errBackendNoExistingWorkspaces{}) { + // We tolerate no workspaces if we're using a state store and + // the default workspace is selected. + ws, err := m.Workspace() + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to check current workspace: %w", err)) + return nil, diags + } + if ws == backend.DefaultStateName { + // If the default workspace is selected, no workspaces existing _may_ be expected. + // It's valid for the default workspace's state to not be created until the first apply takes place. + // However, it could be that the user is configuring their working directory for the first time but + // they expect pre-existing state to be in the store from previous actions. In that case, the user + // should realise their mistake once they generate a plan. + // + // So here, we will just ignore the error. + } else { + // User needs to run a `terraform workspace new` command to create the missing custom workspace. + diags = diags.Append(err) + return nil, diags + } + } else { + // Report all other errors + diags = diags.Append(err) + return nil, diags + } } } @@ -2457,11 +2476,8 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend // Verify that selected workspace exists in the state store. if opts.Init && b != nil { - err := m.selectWorkspace(b) - if err != nil { + if err := m.selectWorkspace(b); err != nil { if errors.Is(err, &errBackendNoExistingWorkspaces{}) { - // If there are no workspaces, Terraform either needs to create the default workspace here - // or instruct the user to run a `terraform workspace new` command. ws, err := m.Workspace() if err != nil { diags = diags.Append(fmt.Errorf("Failed to check current workspace: %w", err)) @@ -2469,21 +2485,13 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend } if ws == backend.DefaultStateName { - // Users control if the default workspace is created through the -create-default-workspace flag (defaults to true) - if opts.CreateDefaultWorkspace { - diags = diags.Append(m.createDefaultWorkspace(c, b)) - if !diags.HasErrors() { - // Report workspace creation to the view - view := views.NewInit(vt, m.View) - view.Output(views.DefaultWorkspaceCreatedMessage) - } - } else { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "The default workspace does not exist", - Detail: "Terraform has been configured to skip creation of the default workspace in the state store. To create it, either remove the `-create-default-workspace=false` flag and re-run the 'init' command, or create it using a 'workspace new' command", - }) - } + // If the default workspace is selected, no workspaces existing _may_ be expected. + // It's valid for the default workspace's state to not be created until the first apply takes place. + // However, it could be that the user is configuring their working directory for the first time but + // they expect pre-existing state to be in the store from previous actions. In that case, the user + // should realise their mistake once they generate a plan. + // + // So here, we will just ignore the error. } else { // User needs to run a `terraform workspace new` command to create the missing custom workspace. diags = append(diags, tfdiags.Sourceless( @@ -2813,35 +2821,6 @@ To make the initial dependency selections that will initialize the dependency lo return pVersion, diags } -// createDefaultWorkspace receives a backend made using a pluggable state store, and details about that store's config, -// and persists an empty state file in the default workspace. By creating this artifact we ensure that the default -// workspace is created and usable by Terraform in later operations. -func (m *Meta) createDefaultWorkspace(c *configs.StateStore, b backend.Backend) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - defaultSMgr, sDiags := b.StateMgr(backend.DefaultStateName) - diags = diags.Append(sDiags) - if sDiags.HasErrors() { - diags = diags.Append(fmt.Errorf("Failed to create a state manager for state store %q in provider %s (%q). This is a bug in Terraform and should be reported: %w", - c.Type, - c.Provider.Name, - c.ProviderAddr, - sDiags.Err())) - return diags - } - emptyState := states.NewState() - if err := defaultSMgr.WriteState(emptyState); err != nil { - diags = diags.Append(errStateStoreWorkspaceCreateDiag(err, c.Type)) - return diags - } - if err := defaultSMgr.PersistState(nil); err != nil { - diags = diags.Append(errStateStoreWorkspaceCreateDiag(err, c.Type)) - return diags - } - - return diags -} - // Initializing a saved state store from the backend state file (aka 'cache file', aka 'legacy state file') func (m *Meta) savedStateStore(sMgr *clistate.LocalState) (backend.Backend, tfdiags.Diagnostics) { // We're preparing a state_store version of backend.Backend. diff --git a/internal/command/meta_backend_errors.go b/internal/command/meta_backend_errors.go index e09356974f..d81284205b 100644 --- a/internal/command/meta_backend_errors.go +++ b/internal/command/meta_backend_errors.go @@ -280,23 +280,6 @@ If the backend already contains existing workspaces, you may need to update the backend configuration.` } -func errStateStoreWorkspaceCreateDiag(innerError error, storeType string) tfdiags.Diagnostic { - msg := fmt.Sprintf(`Error creating the default workspace using pluggable state store %s: %s - -This could be a bug in the provider used for state storage, or a bug in -Terraform. Please file an issue with the provider developers before reporting -a bug for Terraform.`, - storeType, - innerError, - ) - - return tfdiags.Sourceless( - tfdiags.Error, - "Cannot create the default workspace", - msg, - ) -} - // migrateOrReconfigDiag creates a diagnostic to present to users when // an init command encounters a mismatch in backend state and the current config // and Terraform needs users to provide additional instructions about how Terraform diff --git a/internal/command/workspace_command_test.go b/internal/command/workspace_command_test.go index ca90168399..55c02751de 100644 --- a/internal/command/workspace_command_test.go +++ b/internal/command/workspace_command_test.go @@ -31,6 +31,10 @@ func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) { mock := testStateStoreMockWithChunkNegotiation(t, 1000) + // Mock that a custom workspace already exists. + preExistingState := "pre-existing" + mock.MockStates = map[string]interface{}{preExistingState: true} + // Assumes the mocked provider is hashicorp/test providerSource, close := newMockProviderSource(t, map[string][]string{ "hashicorp/test": {"1.2.3"}, @@ -60,9 +64,9 @@ func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) { if code != 0 { t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter) } - // We expect a state to have been created for the default workspace - if _, ok := mock.MockStates["default"]; !ok { - t.Fatal("expected the default workspace to exist, but it didn't") + // We expect a state to have not been created for the default workspace + if _, ok := mock.MockStates["default"]; ok { + t.Fatal("expected the default workspace to not exist, but it did") } //// Create Workspace @@ -73,9 +77,12 @@ func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) { Meta: meta, } - current, _ := newCmd.Workspace() - if current != backend.DefaultStateName { - t.Fatal("before creating any custom workspaces, the current workspace should be 'default'") + current, err := newCmd.Workspace() + if err != nil { + t.Fatal(err) + } + if current != preExistingState { + t.Fatalf("before creating any custom workspaces, the current workspace should be %q, got: %q", preExistingState, current) } args = []string{newWorkspace} @@ -117,7 +124,7 @@ func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) { selCmd := &WorkspaceSelectCommand{ Meta: meta, } - selectedWorkspace := backend.DefaultStateName + selectedWorkspace := preExistingState args = []string{selectedWorkspace} code = selCmd.Run(args) if code != 0 { @@ -145,8 +152,8 @@ func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) { } current, _ = newCmd.Workspace() - if current != backend.DefaultStateName { - t.Fatal("current workspace should be 'default'") + if current != preExistingState { + t.Fatalf("current workspace should be %q, got %q", preExistingState, current) } //// Delete Workspace