From 5be24bf1d067cae6ea84d27899b6b5005ef3032f Mon Sep 17 00:00:00 2001 From: Sarah French <15078782+SarahFrench@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:23:35 +0100 Subject: [PATCH] Fix experimental gating of PSS feature (#37526) * Stop `state_store` being parsed when experiments aren't enabled * Update tests that show the feature is experimentally gated * Refactor to use unparsed hcl.Block for diagnostics when experiments aren't enabled --- internal/command/experimental_test.go | 104 ++++++++++++++++++++++++++ internal/command/init_test.go | 30 -------- internal/configs/parser_config.go | 18 ++++- 3 files changed, 118 insertions(+), 34 deletions(-) create mode 100644 internal/command/experimental_test.go diff --git a/internal/command/experimental_test.go b/internal/command/experimental_test.go new file mode 100644 index 0000000000..2b4087d931 --- /dev/null +++ b/internal/command/experimental_test.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/cli" +) + +func TestInit_stateStoreBlockIsExperimental(t *testing.T) { + + t.Run("init command", 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) + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: false, + }, + } + + args := []string{} + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("unexpected output: \n%s", testOutput.All()) + } + + // Check output + output := testOutput.Stderr() + if !strings.Contains(output, `Blocks of type "state_store" are not expected here`) { + t.Fatalf("doesn't look like experiment is blocking access': %s", output) + } + }) + + t.Run("non-init command: plan", 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) + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: false, + }, + } + + args := []string{} + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("unexpected output: \n%s", testOutput.All()) + } + + // Check output + output := testOutput.Stderr() + if !strings.Contains(output, `Blocks of type "state_store" are not expected here`) { + t.Fatalf("doesn't look like experiment is blocking access': %s", output) + } + }) + + t.Run("non-init command: state list", 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) + t.Chdir(td) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &StateListCommand{ + Meta: Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: false, + }, + } + + args := []string{} + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("unexpected output: \n%s", testOutput.All()) + } + + // Check output + output := ui.ErrorWriter.String() + if !strings.Contains(output, `Blocks of type "state_store" are not expected here`) { + t.Fatalf("doesn't look like experiment is blocking access': %s", output) + } + }) +} diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 9de0c76f8e..361702972b 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3227,36 +3227,6 @@ func TestInit_testsWithModule(t *testing.T) { } } -func TestInit_stateStoreBlockIsExperimental(t *testing.T) { - // Create a temporary working directory that is empty - td := t.TempDir() - testCopyDir(t, testFixturePath("init-with-state-store"), td) - t.Chdir(td) - - ui := new(cli.MockUi) - view, done := testView(t) - c := &InitCommand{ - Meta: Meta{ - Ui: ui, - View: view, - AllowExperimentalFeatures: false, - }, - } - - args := []string{} - code := c.Run(args) - testOutput := done(t) - if code != 1 { - t.Fatalf("unexpected output: \n%s", testOutput.All()) - } - - // Check output - output := testOutput.Stderr() - if !strings.Contains(output, `Blocks of type "state_store" are not expected here`) { - t.Fatalf("doesn't look like experiment is blocking access': %s", output) - } -} - // newMockProviderSource is a helper to succinctly construct a mock provider // source that contains a set of packages matching the given provider versions // that are available for installation (from temporary local files). diff --git a/internal/configs/parser_config.go b/internal/configs/parser_config.go index 4047fe1db8..0b1aeac702 100644 --- a/internal/configs/parser_config.go +++ b/internal/configs/parser_config.go @@ -122,10 +122,20 @@ func parseConfigFile(body hcl.Body, diags hcl.Diagnostics, override, allowExperi } case "state_store": - stateStoreCfg, cfgDiags := decodeStateStoreBlock(innerBlock) - diags = append(diags, cfgDiags...) - if stateStoreCfg != nil { - file.StateStores = append(file.StateStores, stateStoreCfg) + if allowExperiments { + stateStoreCfg, cfgDiags := decodeStateStoreBlock(innerBlock) + diags = append(diags, cfgDiags...) + if stateStoreCfg != nil { + file.StateStores = append(file.StateStores, stateStoreCfg) + } + } else { + // Prevent parsing of state_store blocks in all commands unless experiments enabled. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported block type", + Detail: "Blocks of type \"state_store\" are not expected here.", + Subject: &innerBlock.TypeRange, + }) } case "cloud": cloudCfg, cfgDiags := decodeCloudBlock(innerBlock)