PSS: Remove automatic creation of the default workspace's state file during `init` (#38281)

* refactor: Stop Terraform creating the default workspace during init when using PSS

This is part of reconciling how in the past backends always reported that the default backend existed, even when it didn't. We want state stores to report reality only, so we need to let Terraform handle the discrepancy. Prior to this commit we handled it by making reality match the old lie that the default workspace always exists. After this commit we're just embracing Terraform working with truthful information.
pull/38293/head
Sarah French 2 months ago committed by GitHub
parent fd6b53b4f1
commit 2dbb7d9c05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 {

@ -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 {

@ -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")

@ -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)
}

@ -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)
}

@ -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

@ -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.

@ -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

@ -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

Loading…
Cancel
Save