diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go index 6b2cb008d0c1..ba113667777d 100644 --- a/internal/command/arguments/init.go +++ b/internal/command/arguments/init.go @@ -75,11 +75,6 @@ type Init struct { Args []string - // The -enable-pluggable-state-storage-experiment flag is used in control flow logic in the init command. - // 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 @@ -118,9 +113,6 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti 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") - if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -129,10 +121,6 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti )) } - if v := os.Getenv("TF_ENABLE_PLUGGABLE_STATE_STORAGE"); v != "" { - 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, @@ -140,33 +128,13 @@ func ParseInit(args []string, experimentsEnabled bool) (*Init, tfdiags.Diagnosti init.CreateDefaultWorkspace = false } - if !experimentsEnabled { - // If experiments aren't enabled then these flags should not be used. - if init.EnablePssExperiment { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled", - "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 !experimentsEnabled && !init.CreateDefaultWorkspace { + // CreateDefaultWorkspace can only be set to false by using the flag + 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.", + )) } if init.MigrateState && init.Json { diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go index 53205da52a6f..bd905eaffd4a 100644 --- a/internal/command/arguments/init_test.go +++ b/internal/command/arguments/init_test.go @@ -182,18 +182,6 @@ func TestParseInit_experimentalFlags(t *testing.T) { wantErr string experimentsEnabled bool }{ - "error: -enable-pluggable-state-storage-experiment and experiments are disabled": { - args: []string{"-enable-pluggable-state-storage-experiment"}, - experimentsEnabled: false, - wantErr: "Cannot use -enable-pluggable-state-storage-experiment flag without experiments enabled", - }, - "error: TF_ENABLE_PLUGGABLE_STATE_STORAGE is set and experiments are disabled": { - envs: map[string]string{ - "TF_ENABLE_PLUGGABLE_STATE_STORAGE": "1", - }, - 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, @@ -206,18 +194,6 @@ func TestParseInit_experimentalFlags(t *testing.T) { 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 92238bd0fd3b..9af33c3eace4 100644 --- a/internal/command/e2etest/pluggable_state_store_test.go +++ b/internal/command/e2etest/pluggable_state_store_test.go @@ -51,7 +51,7 @@ func TestPrimary_stateStore_workspaceCmd(t *testing.T) { } //// Init - _, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") + _, stderr, err := tf.Run("init", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected error: %s\nstderr:\n%s", err, stderr) } diff --git a/internal/command/e2etest/primary_test.go b/internal/command/e2etest/primary_test.go index ebffa9c37d75..15d1180133e4 100644 --- a/internal/command/e2etest/primary_test.go +++ b/internal/command/e2etest/primary_test.go @@ -270,7 +270,7 @@ func TestPrimary_stateStore(t *testing.T) { } //// INIT - stdout, stderr, err := tf.Run("init", "-enable-pluggable-state-storage-experiment=true", "-plugin-dir=cache", "-no-color") + stdout, stderr, err := tf.Run("init", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) } @@ -350,7 +350,7 @@ 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") + stdout, stderr, err := tf.Run("init", "-plugin-dir=cache", "-no-color") if err != nil { t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) } diff --git a/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf b/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf index d2c773a6fca9..aed9857fdb82 100644 --- a/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf +++ b/internal/command/e2etest/testdata/full-workflow-with-state-store-fs/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { simple6 = { source = "registry.terraform.io/hashicorp/simple6" diff --git a/internal/command/e2etest/testdata/full-workflow-with-state-store-inmem/main.tf b/internal/command/e2etest/testdata/full-workflow-with-state-store-inmem/main.tf index 14142c851de4..cb061fc3d170 100644 --- a/internal/command/e2etest/testdata/full-workflow-with-state-store-inmem/main.tf +++ b/internal/command/e2etest/testdata/full-workflow-with-state-store-inmem/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { simple6 = { source = "registry.terraform.io/hashicorp/simple6" diff --git a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf index d2c773a6fca9..935057a15f0d 100644 --- a/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf +++ b/internal/command/e2etest/testdata/initialized-directory-with-state-store-fs/main.tf @@ -1,4 +1,6 @@ terraform { + experiments = [pluggable_state_stores] + required_providers { simple6 = { source = "registry.terraform.io/hashicorp/simple6" diff --git a/internal/command/experimental_test.go b/internal/command/experimental_test.go index 2b4087d93145..83d97a0b731b 100644 --- a/internal/command/experimental_test.go +++ b/internal/command/experimental_test.go @@ -12,8 +12,43 @@ import ( func TestInit_stateStoreBlockIsExperimental(t *testing.T) { - t.Run("init command", func(t *testing.T) { - // Create a temporary working directory with state_store in use + // When experiments are enabled, users are prompted to add experiments = [pluggable_state_stores] to their config. + t.Run("init command without `pluggable_state_stores` experiment in config", func(t *testing.T) { + // Create a temporary working directory with state_store in use but experiment not declared + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-state-store-no-experiment"), td) + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + }, + } + + args := []string{} + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("unexpected output: \n%s", testOutput.All()) + } + + // Check output + output := cleanString(testOutput.Stderr()) + if !strings.Contains(output, `Error: Pluggable state store experiment not supported`) { + t.Fatalf("doesn't look like experiment is blocking access': %s", output) + } + if !strings.Contains(output, "opt into the \"pluggable_state_stores\" experiment using the `terraform` block's `experiments` attribute") { + t.Fatalf("expected the error to explain the need for a config change': %s", output) + } + }) + + // When experiments aren't enabled, the state_store block is reported as being unexpected + t.Run("init command without experiments enabled", func(t *testing.T) { + // Create a temporary working directory with state_store in use but experiment not declared td := t.TempDir() testCopyDir(t, testFixturePath("init-with-state-store"), td) t.Chdir(td) @@ -42,7 +77,7 @@ func TestInit_stateStoreBlockIsExperimental(t *testing.T) { } }) - t.Run("non-init command: plan", func(t *testing.T) { + t.Run("non-init command: `plan` without experiments enabled", func(t *testing.T) { // Create a temporary working directory with state_store in use td := t.TempDir() testCopyDir(t, testFixturePath("init-with-state-store"), td) @@ -72,7 +107,7 @@ func TestInit_stateStoreBlockIsExperimental(t *testing.T) { } }) - t.Run("non-init command: state list", func(t *testing.T) { + t.Run("non-init command: `state list` without experiments enabled", func(t *testing.T) { // Create a temporary working directory with state_store in use td := t.TempDir() testCopyDir(t, testFixturePath("init-with-state-store"), td) diff --git a/internal/command/init.go b/internal/command/init.go index 4c77dd4a8790..6c54a0b34837 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/providercache" @@ -55,16 +56,38 @@ func (c *InitCommand) Run(args []string) int { return 1 } + // Look for experiments opted-into via the configuration. + // However we need to accommodate when the -from-module flag is used. + // For now, we simply won't parse experiments at this stage when deciding if the PSS experiment is enabled; + // users are unlikely to be consuming the experiment via -from-module and there are complications accommodating that use case. + // + // The variables below will not be set if the -from-module flag is used, and downstream logic will perform parsing once + // the module is downloaded. + var rootMod *configs.Module + var rootModDiags tfdiags.Diagnostics + if initArgs.FromModule == "" { + path, err := ModulePath(initArgs.Args) + if err != nil { + diags = diags.Append(err) + view.Diagnostics(diags) + return 1 + } + rootMod, rootModDiags = c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) + // We purposefully don't exit early if there are error diagnostics returned here; there are errors related to the Terraform version + // that have precedence and are detected downstream. + // We pass the configuration and diagnostic values from here into downstream code, replacing where the files are parsed there. + // This prevents the diagnostics being lost, as re-parsing the same config results in lost diagnostics. + } + // The else condition below invokes the original logic of the init command. // An experimental version of the init code will be used if: - // > The user uses an experimental version of TF (alpha or built from source) - // > Either the flag -enable-pluggable-state-storage-experiment is passed to the init command. - // > Or, the environment variable TF_ENABLE_PLUGGABLE_STATE_STORAGE is set to any value. - if c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment { + // > The user uses an experimental version of TF (alpha or built from source). + // > The terraform block in the configuration lists the `pluggable_state_stores` experiment. + if c.Meta.AllowExperimentalFeatures && rootMod.ActiveExperiments.Has(experiments.PluggableStateStores) { // TODO(SarahFrench/radeksimko): Remove forked init logic once feature is no longer experimental - return c.runPssInit(initArgs, view) + return c.runPssInit(initArgs, view, rootMod, rootModDiags) } else { - return c.run(initArgs, view) + return c.run(initArgs, view, rootMod, rootModDiags) } } @@ -1476,15 +1499,12 @@ Options: -test-directory=path Set the Terraform test directory, defaults to "tests". - -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. + This flag must be used alongside naming the pluggable_state_stores + experiment in your configuration and using an experimental build of + Terraform. 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_run.go b/internal/command/init_run.go index fbc45794f209..2cd4fe9ce4aa 100644 --- a/internal/command/init_run.go +++ b/internal/command/init_run.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -22,7 +23,7 @@ import ( "go.opentelemetry.io/otel/trace" ) -func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { +func (c *InitCommand) run(initArgs *arguments.Init, view views.Init, rootModEarly *configs.Module, earlyConfDiags tfdiags.Diagnostics) int { var diags tfdiags.Diagnostics c.forceInitCopy = initArgs.ForceInitCopy @@ -129,8 +130,13 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { return 0 } - // Load just the root module to begin backend and module initialization - rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) + // TODO(SarahFrench/radeksimko): Once PSS's experiment is over + // restore parsing of config to only happen here in this code instead of calling code. + // + // Here we accommodate use of -from-module; we need to parse config after the module is downloaded. + if initArgs.FromModule != "" && rootModEarly == nil { + rootModEarly, earlyConfDiags = c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) + } // There may be parsing errors in config loading but these will be shown later _after_ // checking for core version requirement errors. Not meeting the version requirement should @@ -142,7 +148,7 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { return 1 } - if !(c.Meta.AllowExperimentalFeatures && initArgs.EnablePssExperiment) && rootModEarly.StateStore != nil { + if !(c.Meta.AllowExperimentalFeatures && rootModEarly.ActiveExperiments.Has(experiments.PluggableStateStores)) && rootModEarly.StateStore != nil { // TODO(SarahFrench/radeksimko) - remove when this feature isn't experimental. // This approach for making the feature experimental is required // to let us assert the feature is gated behind an experiment in tests. @@ -152,11 +158,13 @@ func (c *InitCommand) run(initArgs *arguments.Init, view views.Init) int { if !c.Meta.AllowExperimentalFeatures { detail += " an experimental build of terraform" } - if !initArgs.EnablePssExperiment { + if !rootModEarly.ActiveExperiments.Has(experiments.PluggableStateStores) { if !c.Meta.AllowExperimentalFeatures { detail += " and" } - detail += " -enable-pluggable-state-storage-experiment flag" + detail += fmt.Sprintf(" the configuration to opt into the %q experiment using the `terraform` block's `experiments` attribute", + experiments.PluggableStateStores.Keyword(), + ) } diags = diags.Append(earlyConfDiags) diff --git a/internal/command/init_run_experiment.go b/internal/command/init_run_experiment.go index 34983553f6fb..1d2dad8f938a 100644 --- a/internal/command/init_run_experiment.go +++ b/internal/command/init_run_experiment.go @@ -31,7 +31,7 @@ import ( // `runPssInit` is an altered version of the logic in `run` that contains changes // related to the PSS project. This is used by the (InitCommand.Run method only if Terraform has // experimental features enabled. -func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int { +func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init, rootModEarly *configs.Module, earlyConfDiags tfdiags.Diagnostics) int { var diags tfdiags.Diagnostics c.forceInitCopy = initArgs.ForceInitCopy @@ -138,8 +138,13 @@ func (c *InitCommand) runPssInit(initArgs *arguments.Init, view views.Init) int return 0 } - // Load just the root module to begin backend and module initialization - rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) + // TODO(SarahFrench/radeksimko): Once PSS's experiment is over + // restore parsing of config to only happen here in this code instead of calling code. + // + // Here we accommodate use of -from-module; we need to parse config after the module is downloaded. + if initArgs.FromModule != "" && rootModEarly == nil { + rootModEarly, earlyConfDiags = c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) + } // There may be parsing errors in config loading but these will be shown later _after_ // checking for core version requirement errors. Not meeting the version requirement should diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 850fb7cc06f1..8fe6da4a9bc2 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -226,8 +226,7 @@ func TestInit_two_step_provider_download(t *testing.T) { }, } - args := append(tc.flags, "-enable-pluggable-state-storage-experiment") // Needed to test init changes for PSS project - if code := c.Run(args); code != 0 { + if code := c.Run(tc.flags); code != 0 { t.Fatalf("bad: \n%s", done(t).All()) } @@ -3267,7 +3266,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { Meta: meta, } - args := []string{"-enable-pluggable-state-storage-experiment=true"} + args := []string{} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -3351,7 +3350,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { }, } - args := []string{"-enable-pluggable-state-storage-experiment=true", "-create-default-workspace=false"} + args := []string{"-create-default-workspace=false"} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -3401,7 +3400,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { } t.Setenv("TF_SKIP_CREATE_DEFAULT_WORKSPACE", "1") // any value - args := []string{"-enable-pluggable-state-storage-experiment=true"} + args := []string{} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -3456,7 +3455,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { Meta: meta, } - args := []string{"-enable-pluggable-state-storage-experiment=true"} + args := []string{} code := c.Run(args) testOutput := done(t) if code != 1 { @@ -3530,7 +3529,6 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { // If input is disabled users receive an error about the missing workspace args := []string{ - "-enable-pluggable-state-storage-experiment=true", "-input=false", } code := c.Run(args) @@ -3631,9 +3629,7 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { } // Run init command - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -3703,7 +3699,6 @@ func TestInit_stateStore_configChanges(t *testing.T) { } args := []string{ - "-enable-pluggable-state-storage-experiment=true", "-reconfigure", } code := c.Run(args) @@ -3786,9 +3781,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { Meta: meta, } - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 1 { @@ -3836,9 +3829,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { Meta: meta, } - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 1 { @@ -3888,9 +3879,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { Meta: meta, } - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 1 { @@ -3946,9 +3935,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { Meta: meta, } - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 1 { @@ -4002,7 +3989,6 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) { } args := []string{ - "-enable-pluggable-state-storage-experiment=true", "-upgrade", } code := c.Run(args) @@ -4057,9 +4043,7 @@ func TestInit_stateStore_unset(t *testing.T) { } // Init - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -4099,7 +4083,6 @@ func TestInit_stateStore_unset(t *testing.T) { } args := []string{ - "-enable-pluggable-state-storage-experiment=true", "-force-copy", } code := c.Run(args) @@ -4158,9 +4141,7 @@ func TestInit_stateStore_unset_withoutProviderRequirements(t *testing.T) { } // Init - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -4201,7 +4182,6 @@ func TestInit_stateStore_unset_withoutProviderRequirements(t *testing.T) { } args := []string{ - "-enable-pluggable-state-storage-experiment=true", "-force-copy", } code := c.Run(args) @@ -4256,9 +4236,7 @@ func TestInit_stateStore_to_backend(t *testing.T) { AllowExperimentalFeatures: true, }, } - args := []string{ - "-enable-pluggable-state-storage-experiment=true", - } + args := []string{} code := c.Run(args) testOutput := done(t) if code != 0 { @@ -4376,7 +4354,6 @@ func TestInit_stateStore_to_backend(t *testing.T) { } args := []string{ - "-enable-pluggable-state-storage-experiment=true", "-force-copy", } code := c.Run(args) diff --git a/internal/command/testdata/backend-to-state-store/main.tf b/internal/command/testdata/backend-to-state-store/main.tf index 37fb2ac835a6..fa289c4dba6f 100644 --- a/internal/command/testdata/backend-to-state-store/main.tf +++ b/internal/command/testdata/backend-to-state-store/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/init-provider-download/config-and-state-different-providers/main.tf b/internal/command/testdata/init-provider-download/config-and-state-different-providers/main.tf index f0687030bb2f..edbbb70c0205 100644 --- a/internal/command/testdata/init-provider-download/config-and-state-different-providers/main.tf +++ b/internal/command/testdata/init-provider-download/config-and-state-different-providers/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { null = { source = "hashicorp/null" diff --git a/internal/command/testdata/init-provider-download/config-and-state-same-providers/main.tf b/internal/command/testdata/init-provider-download/config-and-state-same-providers/main.tf index d5857489d6de..049a21c4c1a5 100644 --- a/internal/command/testdata/init-provider-download/config-and-state-same-providers/main.tf +++ b/internal/command/testdata/init-provider-download/config-and-state-same-providers/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { random = { source = "hashicorp/random" diff --git a/internal/command/testdata/init-provider-download/config-state-file-and-lockfile/main.tf b/internal/command/testdata/init-provider-download/config-state-file-and-lockfile/main.tf index d5857489d6de..049a21c4c1a5 100644 --- a/internal/command/testdata/init-provider-download/config-state-file-and-lockfile/main.tf +++ b/internal/command/testdata/init-provider-download/config-state-file-and-lockfile/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { random = { source = "hashicorp/random" diff --git a/internal/command/testdata/init-provider-download/state-file-only/main.tf b/internal/command/testdata/init-provider-download/state-file-only/main.tf index d568a66bf2ed..58beae37714f 100644 --- a/internal/command/testdata/init-provider-download/state-file-only/main.tf +++ b/internal/command/testdata/init-provider-download/state-file-only/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] backend "local" { path = "./state-using-random-provider.tfstate" } diff --git a/internal/command/testdata/init-state-store/main.tf b/internal/command/testdata/init-state-store/main.tf index 9f39a46e775f..ebce8770cd3c 100644 --- a/internal/command/testdata/init-state-store/main.tf +++ b/internal/command/testdata/init-state-store/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] state_store "test_store" { provider "test" {} value = "foobar" diff --git a/internal/command/testdata/init-with-state-store-no-experiment/main.tf b/internal/command/testdata/init-with-state-store-no-experiment/main.tf new file mode 100644 index 000000000000..74980137349e --- /dev/null +++ b/internal/command/testdata/init-with-state-store-no-experiment/main.tf @@ -0,0 +1,14 @@ +terraform { + # There should be `experiments = [pluggable_state_stores]` present here, but it is intentionally missing. + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" { + } + + value = "foobar" + } +} diff --git a/internal/command/testdata/init-with-state-store/main.tf b/internal/command/testdata/init-with-state-store/main.tf index 99f0daa63498..1f82b9de0d8a 100644 --- a/internal/command/testdata/init-with-state-store/main.tf +++ b/internal/command/testdata/init-with-state-store/main.tf @@ -1,5 +1,5 @@ terraform { - + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/plan-out-state-store/main.tf b/internal/command/testdata/plan-out-state-store/main.tf index d38a6c30025e..294d86e85d36 100644 --- a/internal/command/testdata/plan-out-state-store/main.tf +++ b/internal/command/testdata/plan-out-state-store/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-push-state-store-good/main.tf b/internal/command/testdata/state-push-state-store-good/main.tf index 34b58fdc0e2e..5dd6f603e1e4 100644 --- a/internal/command/testdata/state-push-state-store-good/main.tf +++ b/internal/command/testdata/state-push-state-store-good/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "registry.terraform.io/hashicorp/test" diff --git a/internal/command/testdata/state-store-changed/provider-config/main.tf b/internal/command/testdata/state-store-changed/provider-config/main.tf index fefe037c7336..4aa0a52f74e9 100644 --- a/internal/command/testdata/state-store-changed/provider-config/main.tf +++ b/internal/command/testdata/state-store-changed/provider-config/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-changed/provider-upgraded/main.tf b/internal/command/testdata/state-store-changed/provider-upgraded/main.tf index 6ca83e8d2ba4..2f142bb3841a 100644 --- a/internal/command/testdata/state-store-changed/provider-upgraded/main.tf +++ b/internal/command/testdata/state-store-changed/provider-upgraded/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-changed/provider-used/main.tf b/internal/command/testdata/state-store-changed/provider-used/main.tf index f0f5cbff5dd0..551598ece9f7 100644 --- a/internal/command/testdata/state-store-changed/provider-used/main.tf +++ b/internal/command/testdata/state-store-changed/provider-used/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test2 = { source = "hashicorp/test2" diff --git a/internal/command/testdata/state-store-changed/state-store-type/main.tf b/internal/command/testdata/state-store-changed/state-store-type/main.tf index 6db380a2df66..83aa784dd959 100644 --- a/internal/command/testdata/state-store-changed/state-store-type/main.tf +++ b/internal/command/testdata/state-store-changed/state-store-type/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-changed/store-config/main.tf b/internal/command/testdata/state-store-changed/store-config/main.tf index 085c2f36d3f1..55b6c9501413 100644 --- a/internal/command/testdata/state-store-changed/store-config/main.tf +++ b/internal/command/testdata/state-store-changed/store-config/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-new-vars-in-provider/main.tf b/internal/command/testdata/state-store-new-vars-in-provider/main.tf index 79a44f40f8d8..3f99ddb3b1ce 100644 --- a/internal/command/testdata/state-store-new-vars-in-provider/main.tf +++ b/internal/command/testdata/state-store-new-vars-in-provider/main.tf @@ -1,6 +1,7 @@ variable "foo" { default = "bar" } terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-new-vars-in-store/main.tf b/internal/command/testdata/state-store-new-vars-in-store/main.tf index 49d26a185ebc..3293db871fd8 100644 --- a/internal/command/testdata/state-store-new-vars-in-store/main.tf +++ b/internal/command/testdata/state-store-new-vars-in-store/main.tf @@ -1,6 +1,7 @@ variable "foo" { default = "bar" } terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-new/main.tf b/internal/command/testdata/state-store-new/main.tf index 37fb2ac835a6..fa289c4dba6f 100644 --- a/internal/command/testdata/state-store-new/main.tf +++ b/internal/command/testdata/state-store-new/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/testdata/state-store-unchanged/main.tf b/internal/command/testdata/state-store-unchanged/main.tf index df1eaa76b650..99147c6aea1c 100644 --- a/internal/command/testdata/state-store-unchanged/main.tf +++ b/internal/command/testdata/state-store-unchanged/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/command/workspace_command_test.go b/internal/command/workspace_command_test.go index e73dab2907d1..0fd64d9536ea 100644 --- a/internal/command/workspace_command_test.go +++ b/internal/command/workspace_command_test.go @@ -54,7 +54,7 @@ func TestWorkspace_allCommands_pluggableStateStore(t *testing.T) { intCmd := &InitCommand{ Meta: meta, } - args := []string{"-enable-pluggable-state-storage-experiment"} // Needed to test init changes for PSS project + args := []string{} code := intCmd.Run(args) if code != 0 { t.Fatalf("bad: %d\n\n%s\n%s", code, ui.ErrorWriter, ui.OutputWriter) diff --git a/internal/configs/experiments_test.go b/internal/configs/experiments_test.go index 7d0c456784af..7a6fc588d5da 100644 --- a/internal/configs/experiments_test.go +++ b/internal/configs/experiments_test.go @@ -73,7 +73,7 @@ func TestExperimentsConfig(t *testing.T) { t.Errorf("wrong error\n%s", diff) } }) - t.Run("concluded", func(t *testing.T) { + t.Run("unknown", func(t *testing.T) { parser := NewParser(nil) parser.AllowLanguageExperiments(true) _, diags := parser.LoadConfigDir("testdata/experiments/unknown") diff --git a/internal/configs/module_test.go b/internal/configs/module_test.go index c4fc7b4713bc..f30b21d65cd9 100644 --- a/internal/configs/module_test.go +++ b/internal/configs/module_test.go @@ -551,7 +551,12 @@ func TestModule_conflicting_backend_cloud_stateStore(t *testing.T) { t.Fatal("module should have error diags, but does not") } - if got := diags.Error(); !strings.Contains(got, tc.wantMsg) { + if tc.allowExperiments { + if len(diags) != 2 && len(diags.Errs()) != 1 { + t.Fatalf("expected 2 diagnostics (1 error, 1 warning), but got: %#v", diags) + } + } + if got := diags.Errs()[0]; !strings.Contains(got.Error(), tc.wantMsg) { t.Fatalf("expected error to contain %q\nerror was:\n%s", tc.wantMsg, got) } }) @@ -674,9 +679,13 @@ func TestModule_state_store_multiple(t *testing.T) { if !diags.HasErrors() { t.Fatal("module should have error diags, but does not") } + if len(diags.Errs()) != 1 { + t.Fatalf("expected 1 error, but received %d", len(diags.Errs())) + } want := `Duplicate 'state_store' configuration block` - if got := diags.Error(); !strings.Contains(got, want) { + err := diags.Errs()[0] + if got := err.Error(); !strings.Contains(got, want) { t.Fatalf("expected error to contain %q\nerror was:\n%s", want, got) } }) diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index 2fe176ca6822..bca0779d7d10 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -73,8 +73,14 @@ func TestParserLoadConfigDirSuccess(t *testing.T) { } diags = filterDiags } - if len(diags) != 0 { - t.Errorf("unexpected diagnostics") + if diags.HasErrors() { + t.Error("unexpected error diagnostics") + for _, diag := range diags.Errs() { + t.Logf("- %s", diag) + } + } + if len(diags) > 0 { + t.Error("unexpected diagnostics:") for _, diag := range diags { t.Logf("- %s", diag) } diff --git a/internal/configs/testdata/invalid-modules/conflict-cloud-backend-statestore/main.tf b/internal/configs/testdata/invalid-modules/conflict-cloud-backend-statestore/main.tf index c70b1ab5e46a..be5f210234fb 100644 --- a/internal/configs/testdata/invalid-modules/conflict-cloud-backend-statestore/main.tf +++ b/internal/configs/testdata/invalid-modules/conflict-cloud-backend-statestore/main.tf @@ -13,6 +13,8 @@ terraform { source = "hashicorp/test" } } + + experiments = [pluggable_state_stores] state_store "test_store" { provider "test" {} diff --git a/internal/configs/testdata/invalid-modules/conflict-cloud-statestore-separate-files/state-store.tf b/internal/configs/testdata/invalid-modules/conflict-cloud-statestore-separate-files/state-store.tf index 931626e3ea04..97d9d5ee008a 100644 --- a/internal/configs/testdata/invalid-modules/conflict-cloud-statestore-separate-files/state-store.tf +++ b/internal/configs/testdata/invalid-modules/conflict-cloud-statestore-separate-files/state-store.tf @@ -4,6 +4,7 @@ terraform { source = "hashicorp/test" } } + experiments = [pluggable_state_stores] state_store "test_store" { provider "test" {} diff --git a/internal/configs/testdata/invalid-modules/conflict-cloud-statestore/main.tf b/internal/configs/testdata/invalid-modules/conflict-cloud-statestore/main.tf index 9cbc665f79dc..d4761695d99c 100644 --- a/internal/configs/testdata/invalid-modules/conflict-cloud-statestore/main.tf +++ b/internal/configs/testdata/invalid-modules/conflict-cloud-statestore/main.tf @@ -11,6 +11,7 @@ terraform { source = "hashicorp/test" } } + experiments = [pluggable_state_stores] state_store "test_store" { provider "test" {} diff --git a/internal/configs/testdata/invalid-modules/conflict-statestore-backend-separate-files/state-store.tf b/internal/configs/testdata/invalid-modules/conflict-statestore-backend-separate-files/state-store.tf index 931626e3ea04..227389c8a452 100644 --- a/internal/configs/testdata/invalid-modules/conflict-statestore-backend-separate-files/state-store.tf +++ b/internal/configs/testdata/invalid-modules/conflict-statestore-backend-separate-files/state-store.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/configs/testdata/invalid-modules/conflict-statestore-backend/main.tf b/internal/configs/testdata/invalid-modules/conflict-statestore-backend/main.tf index c6f8053397c8..224fe05bcfde 100644 --- a/internal/configs/testdata/invalid-modules/conflict-statestore-backend/main.tf +++ b/internal/configs/testdata/invalid-modules/conflict-statestore-backend/main.tf @@ -1,6 +1,7 @@ terraform { backend "foo" {} + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/configs/testdata/invalid-modules/multiple-state-store/a.tf b/internal/configs/testdata/invalid-modules/multiple-state-store/a.tf index 931626e3ea04..227389c8a452 100644 --- a/internal/configs/testdata/invalid-modules/multiple-state-store/a.tf +++ b/internal/configs/testdata/invalid-modules/multiple-state-store/a.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/configs/testdata/invalid-modules/multiple-state-store/b.tf b/internal/configs/testdata/invalid-modules/multiple-state-store/b.tf index 5d26e9aa0137..ac1fc506ebf5 100644 --- a/internal/configs/testdata/invalid-modules/multiple-state-store/b.tf +++ b/internal/configs/testdata/invalid-modules/multiple-state-store/b.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] state_store "bar_bar" { provider "bar" {} diff --git a/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf b/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf index cc6a1222d019..31e4c03ce7b7 100644 --- a/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf +++ b/internal/configs/testdata/valid-modules/override-backend-with-state-store/override.tf @@ -1,4 +1,6 @@ terraform { + # Not including an experiments list here + # See https://github.com/hashicorp/terraform/issues/38012 required_providers { foo = { source = "my-org/foo" diff --git a/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf b/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf index cc6a1222d019..31e4c03ce7b7 100644 --- a/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf +++ b/internal/configs/testdata/valid-modules/override-cloud-with-state-store/override.tf @@ -1,4 +1,6 @@ terraform { + # Not including an experiments list here + # See https://github.com/hashicorp/terraform/issues/38012 required_providers { foo = { source = "my-org/foo" diff --git a/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf b/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf index 253916cc6d78..03dfd238e110 100644 --- a/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf +++ b/internal/configs/testdata/valid-modules/override-state-store-no-base/override.tf @@ -1,4 +1,6 @@ terraform { + # Not including an experiments list here + # See https://github.com/hashicorp/terraform/issues/38012 required_providers { test = { source = "hashicorp/test" diff --git a/internal/configs/testdata/valid-modules/override-state-store/main.tf b/internal/configs/testdata/valid-modules/override-state-store/main.tf index f4d0aa063ff2..02663756b15d 100644 --- a/internal/configs/testdata/valid-modules/override-state-store/main.tf +++ b/internal/configs/testdata/valid-modules/override-state-store/main.tf @@ -1,4 +1,5 @@ terraform { + experiments = [pluggable_state_stores] required_providers { test = { source = "hashicorp/test" diff --git a/internal/configs/testdata/valid-modules/override-state-store/override.tf b/internal/configs/testdata/valid-modules/override-state-store/override.tf index f305dd5e7487..4989dd0c1a44 100644 --- a/internal/configs/testdata/valid-modules/override-state-store/override.tf +++ b/internal/configs/testdata/valid-modules/override-state-store/override.tf @@ -1,4 +1,6 @@ terraform { + # Not including an experiments list here + # See https://github.com/hashicorp/terraform/issues/38012 required_providers { bar = { source = "my-org/bar" diff --git a/internal/experiments/experiment.go b/internal/experiments/experiment.go index b3cf34f48c00..bec9ffedce01 100644 --- a/internal/experiments/experiment.go +++ b/internal/experiments/experiment.go @@ -25,11 +25,17 @@ const ( PreconditionsPostconditions = Experiment("preconditions_postconditions") EphemeralValues = Experiment("ephemeral_values") UnknownInstances = Experiment("unknown_instances") + PluggableStateStores = Experiment("pluggable_state_stores") ) func init() { // Each experiment constant defined above must be registered here as either // a current or a concluded experiment. + + // Current + registerCurrentExperiment(PluggableStateStores) + + // Concluded registerConcludedExperiment(UnknownInstances, "Unknown instances are being rolled into a larger feature for deferring unready resources and modules.") registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.") registerConcludedExperiment(VariableValidationCrossRef, "Input variable validation rules may now refer to other objects in the same module without enabling any experiment.")