From dff4555514aee63e988c30a5d7c293f29a4ad9f2 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 8 Oct 2025 23:01:11 +0100 Subject: [PATCH 01/38] Pull determining of PSS provider's version from current locks into a separate method --- internal/command/meta_backend.go | 72 +++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 994a95b4ceec..035141310c86 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1691,26 +1691,15 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide } else { // The provider is not built in and is being managed by Terraform // This is the most common scenario, by far. - pLock := opts.Locks.Provider(c.ProviderAddr) - if pLock == nil { - diags = diags.Append(fmt.Errorf("The provider %s (%q) is not present in the lockfile, despite being used for state store %q. This is a bug in Terraform and should be reported.", - c.Provider.Name, - c.ProviderAddr, - c.Type)) - return nil, diags - } - var err error - pVersion, err = providerreqs.GoVersionFromVersion(pLock.Version()) - if err != nil { - diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) when recording backend state for state store %q. This is a bug in Terraform and should be reported: %w", - c.Provider.Name, - c.ProviderAddr, - c.Type, - err)) + var vDiags tfdiags.Diagnostics + pVersion, vDiags = getStateStorageProviderVersionFromLocks(c, opts.Locks) + diags = diags.Append(vDiags) + if vDiags.HasErrors() { return nil, diags } } } + s.StateStore = &workdir.StateStoreConfigState{ Type: c.Type, Hash: uint64(stateStoreHash), @@ -1794,6 +1783,57 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide return b, diags } +// getStateStorageProviderVersionFromLocks assumes that calling code has checked that the provider is fully managed by Terraform, +// and isn't built-in, before using this method. +func getStateStorageProviderVersionFromLocks(c *configs.StateStore, locks *depsfile.Locks) (*version.Version, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var pVersion *version.Version + + pLock := locks.Provider(c.ProviderAddr) + if pLock == nil { + diags = diags.Append(fmt.Errorf("The provider %s (%q) is not present in the lockfile, despite being used for state store %q. This is a bug in Terraform and should be reported.", + c.Provider.Name, + c.ProviderAddr, + c.Type)) + return nil, diags + } + var err error + pVersion, err = providerreqs.GoVersionFromVersion(pLock.Version()) + if err != nil { + diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) when recording backend state for state store %q. This is a bug in Terraform and should be reported: %w", + c.Provider.Name, + c.ProviderAddr, + c.Type, + err)) + return nil, diags + } + + return pVersion, diags +} + +// isProviderReattached determines if a given provider is being supplied to Terraform via the TF_REATTACH_PROVIDERS +// environment variable. +func isProviderReattached(provider addrs.Provider) (bool, error) { + in := os.Getenv("TF_REATTACH_PROVIDERS") + if in != "" { + var m map[string]any + err := json.Unmarshal([]byte(in), &m) + if err != nil { + return false, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err) + } + for p := range m { + a, diags := addrs.ParseProviderSourceString(p) + if diags.HasErrors() { + return false, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err()) + } + if a.Equals(provider) { + return true, nil + } + } + } + return false, nil +} + // 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. From bd41b674eec306613d3638882e09a9adc7afee48 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 8 Oct 2025 23:02:21 +0100 Subject: [PATCH 02/38] Add code for identifying when config and provider version match existing backend state (i.e. no changes) --- internal/command/meta_backend.go | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 035141310c86..47ec83716948 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -944,6 +944,42 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // > Changing how the store is configured. // > Allowing values to be moved between partial overrides and config + // We are not going to migrate if... + // + // The state storage provider is the same + // AND the provider version is the same + // AND the provider config cache hash values match, indicating that the provider config is valid and completely unchanged. + // AND the same state_store implementation is used + // AND the state_store config cache hash values match, indicating that the state_store config is valid and completely unchanged. + // AND we're not providing any overrides. An override can mean a change overriding an unchanged state_store block (indicated by the hash value). + pVersion, vDiags := getStateStorageProviderVersionFromLocks(stateStoreConfig, opts.Locks) + diags = diags.Append(vDiags) + if vDiags.HasErrors() { + return nil, diags + } + + if s.StateStore.Provider.Source.Equals(stateStoreConfig.ProviderAddr) && + s.StateStore.Provider.Version.Equal(pVersion) && + (uint64(stateStoreProviderHash) == s.StateStore.Provider.Hash) && + s.StateStore.Type == stateStoreConfig.Type && + (uint64(stateStoreHash) == s.StateStore.Hash) && + (!opts.Init || opts.ConfigOverride == nil) { + log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q state_store configuration", stateStoreConfig.Type) + savedStateStore, sssDiags := m.savedStateStore(sMgr, opts.ProviderFactory) + diags = diags.Append(sssDiags) + // 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 + } + } + return savedStateStore, diags + } + + // Above caters only for unchanged config + // but this switch case will also handle changes, + // which isn't implemented yet. return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Not implemented yet", From 141478a76878fd3d3c1cc96fbcafac84e98c45a6 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 8 Oct 2025 23:03:01 +0100 Subject: [PATCH 03/38] Update test - locks are now needed before it hits expected error diag return --- internal/command/meta_backend_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index ba766e520598..c6be42958943 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2150,11 +2150,26 @@ func TestMetaBackend_changeConfiguredStateStore(t *testing.T) { // a pluggable state store implementation called "store". mock := testStateStoreMock(t) + // Define some locks to pass in + locks := depsfile.NewLocks() + providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") + constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") + if err != nil { + t.Fatalf("test setup failed when making constraint: %s", err) + } + locks.SetProvider( + providerAddr, + versions.MustParseVersion("9.9.9"), + constraint, + []providerreqs.Hash{""}, + ) + // Get the operations backend _, beDiags := m.Backend(&BackendOpts{ Init: true, StateStoreConfig: mod.StateStore, ProviderFactory: providers.FactoryFixed(mock), + Locks: locks, }) if !beDiags.HasErrors() { t.Fatal("expected an error to be returned during partial implementation of PSS") From 5c77b8d4126460c5552bfaee99d7432033e9d320 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 8 Oct 2025 23:03:39 +0100 Subject: [PATCH 04/38] Add test showing successful init when no config changes are detected. --- internal/command/init_test.go | 63 +++++++++++++++++++ .../.terraform/terraform.tfstate | 18 ++++++ .../testdata/state-store-unchanged/main.tf | 12 ++++ 3 files changed, 93 insertions(+) create mode 100644 internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-unchanged/main.tf diff --git a/internal/command/init_test.go b/internal/command/init_test.go index fed3a5e56b9f..be690633d69a 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3556,6 +3556,69 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { // >>> Currently this is handled at a lower level in `internal/command/meta_backend_test.go` } +// Testing init's behaviors with `state_store` when run in a working directory where the configuration +// doesn't match the backend state file. +func TestInit_stateStore_configUnchanged(t *testing.T) { + t.Run("init is successful when the configuration and backend state match", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that matches the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-unchanged"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + // If the working directory was previously initialized successfully then at least + // one workspace is guaranteed to exist when a user is re-running init with no config + // changes since last init. So this test says `default` exists. + mockProvider.GetStatesResponse = &providers.GetStatesResponse{ + States: []string{"default"}, + } + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + 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() + expectedOutputs := []string{ + "Initializing the state store...", + "Terraform has been successfully initialized!", + } + for _, expected := range expectedOutputs { + if !strings.Contains(output, expected) { + t.Fatalf("expected output to include %q, but got':\n %s", expected, output) + } + } + }) +} + // Testing init's behaviors with `state_store` when run in a working directory where the configuration // doesn't match the backend state file. func TestInit_stateStore_configChanges(t *testing.T) { diff --git a/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate new file mode 100644 index 000000000000..bece5521e4ad --- /dev/null +++ b/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate @@ -0,0 +1,18 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": {}, + "hash": 3976463117 + }, + "hash": 2116468040 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-unchanged/main.tf b/internal/command/testdata/state-store-unchanged/main.tf new file mode 100644 index 000000000000..d32e0d51615a --- /dev/null +++ b/internal/command/testdata/state-store-unchanged/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" {} + + value = "foobar" # matches backend state file + } +} From bda07d4ef2ab1e416629eb053fedf95eb26bd413 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 13 Oct 2025 17:13:29 +0100 Subject: [PATCH 05/38] Update `getStateStorageProviderVersion` to return nil versions for builtin and re-attached providers. This makes comparison easier when determining if config has changed since last init. --- internal/command/meta_backend.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 47ec83716948..c2e67569b541 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -952,7 +952,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // AND the same state_store implementation is used // AND the state_store config cache hash values match, indicating that the state_store config is valid and completely unchanged. // AND we're not providing any overrides. An override can mean a change overriding an unchanged state_store block (indicated by the hash value). - pVersion, vDiags := getStateStorageProviderVersionFromLocks(stateStoreConfig, opts.Locks) + pVersion, vDiags := getStateStorageProviderVersion(stateStoreConfig, opts.Locks) diags = diags.Append(vDiags) if vDiags.HasErrors() { return nil, diags @@ -1728,7 +1728,7 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide // The provider is not built in and is being managed by Terraform // This is the most common scenario, by far. var vDiags tfdiags.Diagnostics - pVersion, vDiags = getStateStorageProviderVersionFromLocks(c, opts.Locks) + pVersion, vDiags = getStateStorageProviderVersion(c, opts.Locks) diags = diags.Append(vDiags) if vDiags.HasErrors() { return nil, diags @@ -1819,12 +1819,22 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide return b, diags } -// getStateStorageProviderVersionFromLocks assumes that calling code has checked that the provider is fully managed by Terraform, -// and isn't built-in, before using this method. -func getStateStorageProviderVersionFromLocks(c *configs.StateStore, locks *depsfile.Locks) (*version.Version, tfdiags.Diagnostics) { +// getStateStorageProviderVersion assumes that calling code has checked whether the provider is fully managed by Terraform, +// or is built-in, before using this method and is prepared to receive a nil Version. +func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks) (*version.Version, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var pVersion *version.Version + isBuiltin := c.ProviderAddr.Hostname == addrs.BuiltInProviderHost + isReattached, err := isProviderReattached(c.ProviderAddr) + if err != nil { + diags = diags.Append(err) + return nil, diags + } + if isBuiltin || isReattached { + return nil, nil // nil Version returned + } + pLock := locks.Provider(c.ProviderAddr) if pLock == nil { diags = diags.Append(fmt.Errorf("The provider %s (%q) is not present in the lockfile, despite being used for state store %q. This is a bug in Terraform and should be reported.", @@ -1833,7 +1843,6 @@ func getStateStorageProviderVersionFromLocks(c *configs.StateStore, locks *depsf c.Type)) return nil, diags } - var err error pVersion, err = providerreqs.GoVersionFromVersion(pLock.Version()) if err != nil { diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) when recording backend state for state store %q. This is a bug in Terraform and should be reported: %w", From c4c5b1b0bc9f31d894c138d3d41e329e74a296f8 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 13 Oct 2025 17:13:50 +0100 Subject: [PATCH 06/38] Add test coverage for `getStateStorageProviderVersion` --- internal/command/meta_backend_test.go | 102 ++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index c6be42958943..9002fd70c91a 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -16,6 +16,7 @@ import ( "github.com/apparentlymart/go-versions/versions" "github.com/hashicorp/cli" + version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfaddr "github.com/hashicorp/terraform-registry-address" @@ -2870,6 +2871,107 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { }) } +func Test_getStateStorageProviderVersion(t *testing.T) { + // Locks only contain hashicorp/test provider + locks := depsfile.NewLocks() + providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") + constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") + if err != nil { + t.Fatalf("test setup failed when making constraint: %s", err) + } + setVersion := versions.MustParseVersion("9.9.9") + locks.SetProvider( + providerAddr, + setVersion, + constraint, + []providerreqs.Hash{""}, + ) + + t.Run("returns the version of the provider represented in the locks", func(t *testing.T) { + c := &configs.StateStore{ + Provider: &configs.Provider{}, + ProviderAddr: tfaddr.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "test"), + } + v, diags := getStateStorageProviderVersion(c, locks) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + + expectedVersion, err := providerreqs.GoVersionFromVersion(setVersion) + if err != nil { + t.Fatalf("test setup failed when making expected version: %s", err) + } + if !v.Equal(expectedVersion) { + t.Fatalf("expected version to be %#v, got %#v", expectedVersion, v) + } + }) + + t.Run("returns a nil version when using a builtin provider", func(t *testing.T) { + c := &configs.StateStore{ + Provider: &configs.Provider{}, + ProviderAddr: tfaddr.NewProvider(addrs.BuiltInProviderHost, addrs.BuiltInProviderNamespace, "test"), + } + v, diags := getStateStorageProviderVersion(c, locks) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + + var expectedVersion *version.Version = nil + if !v.Equal(expectedVersion) { + t.Fatalf("expected version to be %#v, got %#v", expectedVersion, v) + } + }) + + t.Run("returns a nil version when using a re-attached provider", func(t *testing.T) { + t.Setenv("TF_REATTACH_PROVIDERS", `{ + "test": { + "Protocol": "grpc", + "ProtocolVersion": 6, + "Pid": 12345, + "Test": true, + "Addr": { + "Network": "unix", + "String":"/var/folders/xx/abcde12345/T/plugin12345" + } + } + }`) + c := &configs.StateStore{ + Provider: &configs.Provider{}, + ProviderAddr: tfaddr.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "test"), + } + v, diags := getStateStorageProviderVersion(c, locks) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + + var expectedVersion *version.Version = nil + if !v.Equal(expectedVersion) { + t.Fatalf("expected version to be %#v, got %#v", expectedVersion, v) + } + }) + + t.Run("returns an error diagnostic when version info cannot be obtained from locks", func(t *testing.T) { + c := &configs.StateStore{ + Type: "missing-provider_foobar", + Provider: &configs.Provider{ + Name: "missing-provider", + }, + ProviderAddr: tfaddr.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "missing-provider"), + } + _, diags := getStateStorageProviderVersion(c, locks) + if !diags.HasErrors() { + t.Fatal("expected errors but got none") + } + expectMsg := "not present in the lockfile" + if !strings.Contains(diags.Err().Error(), expectMsg) { + t.Fatalf("expected error to include %q but got: %s", + expectMsg, + diags.Err(), + ) + } + }) +} + func testMetaBackend(t *testing.T, args []string) *Meta { var m Meta m.Ui = new(cli.MockUi) From 3aeb44f25874dda1d09816c3b77bb93f98b2cf2f Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 11:36:09 +0100 Subject: [PATCH 07/38] Move testing fixtures around, preparing for different types of changed state_store config changes being tested --- internal/command/init_test.go | 2 +- .../.terraform/terraform.tfstate | 0 .../state-store-changed/{ => store-and-provider-config}/main.tf | 0 .../store-config}/.terraform/terraform.tfstate | 0 .../store-config}/main.tf | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename internal/command/testdata/state-store-changed/{ => store-and-provider-config}/.terraform/terraform.tfstate (100%) rename internal/command/testdata/state-store-changed/{ => store-and-provider-config}/main.tf (100%) rename internal/command/testdata/{state-store-reconfigure => state-store-changed/store-config}/.terraform/terraform.tfstate (100%) rename internal/command/testdata/{state-store-reconfigure => state-store-changed/store-config}/main.tf (100%) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index be690633d69a..22de092d8543 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3626,7 +3626,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { // Create a temporary working directory with state store configuration // that doesn't match the backend state file td := t.TempDir() - testCopyDir(t, testFixturePath("state-store-reconfigure"), td) + testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) t.Chdir(td) mockProvider := mockPluggableStateStorageProvider() diff --git a/internal/command/testdata/state-store-changed/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/store-and-provider-config/.terraform/terraform.tfstate similarity index 100% rename from internal/command/testdata/state-store-changed/.terraform/terraform.tfstate rename to internal/command/testdata/state-store-changed/store-and-provider-config/.terraform/terraform.tfstate diff --git a/internal/command/testdata/state-store-changed/main.tf b/internal/command/testdata/state-store-changed/store-and-provider-config/main.tf similarity index 100% rename from internal/command/testdata/state-store-changed/main.tf rename to internal/command/testdata/state-store-changed/store-and-provider-config/main.tf diff --git a/internal/command/testdata/state-store-reconfigure/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate similarity index 100% rename from internal/command/testdata/state-store-reconfigure/.terraform/terraform.tfstate rename to internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate diff --git a/internal/command/testdata/state-store-reconfigure/main.tf b/internal/command/testdata/state-store-changed/store-config/main.tf similarity index 100% rename from internal/command/testdata/state-store-reconfigure/main.tf rename to internal/command/testdata/state-store-changed/store-config/main.tf From 5f43a22f470539cad2e8d174227cb102ee5e43fd Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 11:36:48 +0100 Subject: [PATCH 08/38] Add test showing that changing the state_store config is detected as a change, but handling this scenario isn't implemented yet --- internal/command/init_test.go | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 22de092d8543..9432d92acf45 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3711,6 +3711,56 @@ func TestInit_stateStore_configChanges(t *testing.T) { } }) + t.Run("handling changed state store config is currently unimplemented", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that doesn't match the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. + 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) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("expected code 1 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedMsg := "Changing a state store configuration is not implemented yet" + if !strings.Contains(output, expectedMsg) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output) + } + + }) + // TODO(SarahFrench/radeksimko): Add more test cases related to changing the // configuration and the forced need for state migration. // More complicated situations might benefit from being separate tests altogether. From 5ede062302edcde91f8d91ab770225e24bf5e058 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 11:59:20 +0100 Subject: [PATCH 09/38] Update hashes in test fixture backend state file to be accurate Previously dummy values were fine, but as tests using hashes to identify changes these values need to be accurate! --- .../store-config/.terraform/terraform.tfstate | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate index 34cf4c7bf6ba..b5fd848bb1a5 100644 --- a/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate @@ -11,8 +11,8 @@ "version": "1.2.3", "source": "registry.terraform.io/hashicorp/test", "config": {}, - "hash": 12345 + "hash": 3976463117 }, - "hash": 12345 + "hash": 2116468040 } } \ No newline at end of file From 690ce232fc2d8bef90d645af644912e98301698e Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:19:00 +0100 Subject: [PATCH 10/38] Update existing test cases so that Terraform uses the same test provider version as described in the backend state file fixture for the test. --- internal/command/init_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 9432d92acf45..88d0b634af43 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3575,7 +3575,7 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { } mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.2.3"}, + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture }) defer close() @@ -3632,7 +3632,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { mockProvider := mockPluggableStateStorageProvider() mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture }) defer close() @@ -3722,7 +3722,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture }) defer close() From 0d6e33b478cf262bd740bbfa69c4fd2c973af244 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:19:54 +0100 Subject: [PATCH 11/38] Add test showing that changing the PSS provider's config is detected as a change, but handling this scenario isn't implemented yet --- internal/command/init_test.go | 55 +++++++++++++++++-- .../.terraform/terraform.tfstate | 20 +++++++ .../provider-config/main.tf | 14 +++++ 3 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-changed/provider-config/main.tf diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 88d0b634af43..cdb02b38da4d 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3761,12 +3761,55 @@ func TestInit_stateStore_configChanges(t *testing.T) { }) - // TODO(SarahFrench/radeksimko): Add more test cases related to changing the - // configuration and the forced need for state migration. - // More complicated situations might benefit from being separate tests altogether. - // Simpler scenarios that make sense to keep here are: - // 1) Changing config of the same state_store type - // 2) Changing config of the same provider (and version) used for PSS + t.Run("handling changed state store provider config is currently unimplemented", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that doesn't match the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed/provider-config"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("expected code 1 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedMsg := "Changing a state store configuration is not implemented yet" + if !strings.Contains(output, expectedMsg) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output) + } + }) + } // newMockProviderSource is a helper to succinctly construct a mock provider diff --git a/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate new file mode 100644 index 000000000000..1a09f5af080d --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate @@ -0,0 +1,20 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": null + }, + "hash": 3976463117 + }, + "hash": 2116468040 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/provider-config/main.tf b/internal/command/testdata/state-store-changed/provider-config/main.tf new file mode 100644 index 000000000000..fefe037c7336 --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-config/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_store" { + provider "test" { + region = "new-value" # changed versus backend state file + } + + value = "foobar" + } +} From 7899855eb686a971214f25f09789ff5a4f8a3797 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:20:56 +0100 Subject: [PATCH 12/38] Add test showing that swapping to a different state storage implementation in the same provider is detected as a change, but handling this scenario isn't implemented yet --- internal/command/init_test.go | 52 +++++++++++++++++++ .../.terraform/terraform.tfstate | 20 +++++++ .../state-store-type/main.tf | 14 +++++ 3 files changed, 86 insertions(+) create mode 100644 internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-changed/state-store-type/main.tf diff --git a/internal/command/init_test.go b/internal/command/init_test.go index cdb02b38da4d..5d009e50238d 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3810,6 +3810,58 @@ func TestInit_stateStore_configChanges(t *testing.T) { } }) + t.Run("handling changed state store type in the same provider is currently unimplemented", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that doesn't match the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed/state-store-type"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + storeName := "test_store" + otherStoreName := "test_otherstore" + // Make the provider report that it contains a 2nd storage implementation with the above name + mockProvider.GetProviderSchemaResponse.StateStores[otherStoreName] = mockProvider.GetProviderSchemaResponse.StateStores[storeName] + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("expected code 1 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedMsg := "Changing a state store configuration is not implemented yet" + if !strings.Contains(output, expectedMsg) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output) + } + }) + } // newMockProviderSource is a helper to succinctly construct a mock provider diff --git a/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate new file mode 100644 index 000000000000..f875064036af --- /dev/null +++ b/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate @@ -0,0 +1,20 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": "foobar" + }, + "hash": 2116468040 + }, + "hash": 2116468040 + } +} \ No newline at end of file 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 new file mode 100644 index 000000000000..6db380a2df66 --- /dev/null +++ b/internal/command/testdata/state-store-changed/state-store-type/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + state_store "test_otherstore" { # changed store type versus backend state file; test_otherstore versus test_store + provider "test" { + region = "foobar" + } + + value = "foobar" + } +} From e8579b9e3e6d60d5041332363168b0de536f9244 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:23:49 +0100 Subject: [PATCH 13/38] Add test showing that changing the provider used for PSS is detected as a change, but handling this scenario isn't implemented yet --- internal/command/init_test.go | 57 +++++++++++++++++++ .../.terraform/terraform.tfstate | 20 +++++++ .../state-store-changed/provider-used/main.tf | 16 ++++++ 3 files changed, 93 insertions(+) create mode 100644 internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-changed/provider-used/main.tf diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 5d009e50238d..92bff120ea52 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3862,6 +3862,63 @@ func TestInit_stateStore_configChanges(t *testing.T) { } }) + t.Run("handling changing the provider used for state storage is currently unimplemented", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that doesn't match the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed/provider-used"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} // The previous init implied by this test scenario would have created the default workspace. + + // Make a mock that implies its name is test2 based on returned schemas + mockProvider2 := mockPluggableStateStorageProvider() + mockProvider2.GetProviderSchemaResponse.StateStores["test2_store"] = mockProvider.GetProviderSchemaResponse.StateStores["test_store"] + delete(mockProvider2.GetProviderSchemaResponse.StateStores, "test_store") + + mockProviderAddress := addrs.NewDefaultProvider("test") + mockProviderAddress2 := addrs.NewDefaultProvider("test2") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3"}, // Provider in backend state file fixture + "hashicorp/test2": {"1.2.3"}, // Provider now used in config + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), // test provider + mockProviderAddress2: providers.FactoryFixed(mockProvider2), // test2 provider + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("expected code 1 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedMsg := "Changing a state store configuration is not implemented yet" + if !strings.Contains(output, expectedMsg) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output) + } + }) } // newMockProviderSource is a helper to succinctly construct a mock provider diff --git a/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate new file mode 100644 index 000000000000..f875064036af --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate @@ -0,0 +1,20 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": "foobar" + }, + "hash": 2116468040 + }, + "hash": 2116468040 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/provider-used/main.tf b/internal/command/testdata/state-store-changed/provider-used/main.tf new file mode 100644 index 000000000000..f0f5cbff5dd0 --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-used/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + test2 = { + source = "hashicorp/test2" + } + } + + # changed to using `test2` provider, versus `test` used in the backend state file + state_store "test2_store" { + provider "test2" { + region = "foobar" + } + + value = "foobar" + } +} From d1146c85e58824a32f78c042b5ac4f89b0af06d7 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:24:41 +0100 Subject: [PATCH 14/38] Add test showing that upgrading a provider is detected as a change, but handling this scenario isn't implemented yet --- internal/command/init_test.go | 52 +++++++++++++++++++ .../.terraform/terraform.tfstate | 20 +++++++ .../provider-upgraded/main.tf | 15 ++++++ 3 files changed, 87 insertions(+) create mode 100644 internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate create mode 100644 internal/command/testdata/state-store-changed/provider-upgraded/main.tf diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 92bff120ea52..ae2205767d84 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3921,6 +3921,58 @@ func TestInit_stateStore_configChanges(t *testing.T) { }) } +// Testing init's behaviors with `state_store` when the provider used for state storage in a previous init +// command is updated. +func TestInit_stateStore_providerUpgrade(t *testing.T) { + t.Run("handling upgrading the provider used for state storage is currently unimplemented", func(t *testing.T) { + // Create a temporary working directory with state store configuration + // that doesn't match the backend state file + td := t.TempDir() + testCopyDir(t, testFixturePath("state-store-changed/provider-upgraded"), td) + t.Chdir(td) + + mockProvider := mockPluggableStateStorageProvider() + mockProviderAddress := addrs.NewDefaultProvider("test") + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.2.3", "9.9.9"}, // 1.2.3 is the version used in the backend state file, 9.9.9 is the version being upgraded to + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + meta := Meta{ + Ui: ui, + View: view, + AllowExperimentalFeatures: true, + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + mockProviderAddress: providers.FactoryFixed(mockProvider), + }, + }, + ProviderSource: providerSource, + } + c := &InitCommand{ + Meta: meta, + } + + args := []string{ + "-enable-pluggable-state-storage-experiment=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("expected code 1 exit code, got %d, output: \n%s", code, testOutput.All()) + } + + // Check output + output := testOutput.All() + expectedMsg := "Changing a state store configuration is not implemented yet" + if !strings.Contains(output, expectedMsg) { + t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, 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/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate new file mode 100644 index 000000000000..f875064036af --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate @@ -0,0 +1,20 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "state_store": { + "type": "test_store", + "config": { + "value": "foobar" + }, + "provider": { + "version": "1.2.3", + "source": "registry.terraform.io/hashicorp/test", + "config": { + "region": "foobar" + }, + "hash": 2116468040 + }, + "hash": 2116468040 + } +} \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/provider-upgraded/main.tf b/internal/command/testdata/state-store-changed/provider-upgraded/main.tf new file mode 100644 index 000000000000..d8028e8c9918 --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-upgraded/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + # version = "9.9.9" // We've now specified using v9.9.9, versus the v1.2.3 used at last init and in the backend state file + } + } + state_store "test_store" { + provider "test" { + region = "foobar" + } + + value = "foobar" + } +} From eb205a72fe2e5dc6b263f5aa0a5ee438ac95763b Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:35:42 +0100 Subject: [PATCH 15/38] Update test to use v1.2.3 for consistency with other tests Just to avoid any confusion if copy-pasting happens in future. --- internal/command/init_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index ae2205767d84..ef8ba143fb9c 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3242,7 +3242,9 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { mockProvider := mockPluggableStateStorageProvider() mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ - "hashicorp/test": {"1.0.0"}, + // The test fixture config has no version constraints, so the latest version will + // be used; below is the 'latest' version in the test world. + "hashicorp/test": {"1.2.3"}, }) defer close() @@ -3298,13 +3300,13 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { if s == nil { t.Fatal("expected backend state file to be created, but there isn't one") } - v1_0_0, _ := version.NewVersion("1.0.0") + v1_2_3, _ := version.NewVersion("1.2.3") expectedState := &workdir.StateStoreConfigState{ Type: "test_store", ConfigRaw: []byte("{\n \"value\": \"foobar\"\n }"), Hash: uint64(2116468040), // Hash affected by config Provider: &workdir.ProviderConfigState{ - Version: v1_0_0, + Version: v1_2_3, Source: &tfaddr.Provider{ Hostname: tfaddr.DefaultProviderRegistryHost, Namespace: "hashicorp", @@ -3690,13 +3692,13 @@ func TestInit_stateStore_configChanges(t *testing.T) { if s == nil { t.Fatal("expected backend state file to be created, but there isn't one") } - v1_0_0, _ := version.NewVersion("1.0.0") + v1_2_3, _ := version.NewVersion("1.2.3") expectedState := &workdir.StateStoreConfigState{ Type: "test_store", ConfigRaw: []byte("{\n \"value\": \"changed-value\"\n }"), Hash: uint64(1417640992), // Hash affected by config Provider: &workdir.ProviderConfigState{ - Version: v1_0_0, + Version: v1_2_3, Source: &tfaddr.Provider{ Hostname: tfaddr.DefaultProviderRegistryHost, Namespace: "hashicorp", From 3e5495ead55ed6e91c82c92210976a8e679416a5 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 12:43:38 +0100 Subject: [PATCH 16/38] More corrections to existing test fixtures - unset config should be null, and replace dummy hash values with correct values. --- .../store-config/.terraform/terraform.tfstate | 4 +++- .../state-store-to-backend/.terraform/terraform.tfstate | 8 +++++--- .../state-store-unchanged/.terraform/terraform.tfstate | 4 +++- .../state-store-unset/.terraform/terraform.tfstate | 8 +++++--- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate index b5fd848bb1a5..a2292b07b998 100644 --- a/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate @@ -10,7 +10,9 @@ "provider": { "version": "1.2.3", "source": "registry.terraform.io/hashicorp/test", - "config": {}, + "config": { + "region": null + }, "hash": 3976463117 }, "hash": 2116468040 diff --git a/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate b/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate index cfb4e3d72ade..1a09f5af080d 100644 --- a/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate @@ -10,9 +10,11 @@ "provider": { "version": "1.2.3", "source": "registry.terraform.io/hashicorp/test", - "config": {}, - "hash": 12345 + "config": { + "region": null + }, + "hash": 3976463117 }, - "hash": 12345 + "hash": 2116468040 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate index bece5521e4ad..1a09f5af080d 100644 --- a/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate @@ -10,7 +10,9 @@ "provider": { "version": "1.2.3", "source": "registry.terraform.io/hashicorp/test", - "config": {}, + "config": { + "region": null + }, "hash": 3976463117 }, "hash": 2116468040 diff --git a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate index cfb4e3d72ade..1a09f5af080d 100644 --- a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate @@ -10,9 +10,11 @@ "provider": { "version": "1.2.3", "source": "registry.terraform.io/hashicorp/test", - "config": {}, - "hash": 12345 + "config": { + "region": null + }, + "hash": 3976463117 }, - "hash": 12345 + "hash": 2116468040 } } \ No newline at end of file From b485cd89d2103ffc8af24e93e56c87f96a0cfaa0 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 14:33:22 +0100 Subject: [PATCH 17/38] Fix test for using -reconfigure with state_store; the default workspace would already exist in this scenario --- internal/command/init_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index ef8ba143fb9c..00818d9b01ec 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3632,6 +3632,11 @@ func TestInit_stateStore_configChanges(t *testing.T) { t.Chdir(td) mockProvider := mockPluggableStateStorageProvider() + + // The previous init implied by this test scenario would have created this. + mockProvider.GetStatesResponse = &providers.GetStatesResponse{States: []string{"default"}} + mockProvider.MockStates = map[string]interface{}{"default": true} + mockProviderAddress := addrs.NewDefaultProvider("test") providerSource, close := newMockProviderSource(t, map[string][]string{ "hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture @@ -3677,11 +3682,6 @@ func TestInit_stateStore_configChanges(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 contents of the backend state file statePath := filepath.Join(meta.DataDir(), DefaultStateFilename) sMgr := &clistate.LocalState{Path: statePath} From 0b3e7590e386319f28fb493792049c8db3a0fe12 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 14:33:51 +0100 Subject: [PATCH 18/38] Update TestInit_stateStore_configUnchanged to assert that init was a no-op for backend state --- internal/command/init_test.go | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 00818d9b01ec..102f991e000c 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3561,6 +3561,24 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { // Testing init's behaviors with `state_store` when run in a working directory where the configuration // doesn't match the backend state file. func TestInit_stateStore_configUnchanged(t *testing.T) { + // This matches the backend state test fixture in "state-store-unchanged" + v1_2_3, _ := version.NewVersion("1.2.3") + expectedState := &workdir.StateStoreConfigState{ + Type: "test_store", + ConfigRaw: []byte("{\n \"value\": \"foobar\"\n }"), + Hash: uint64(2116468040), // Hash affected by config + Provider: &workdir.ProviderConfigState{ + Version: v1_2_3, + Source: &tfaddr.Provider{ + Hostname: tfaddr.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }, + ConfigRaw: []byte("{\n \"region\": null\n }"), + Hash: uint64(3976463117), // Hash of empty config + }, + } + t.Run("init is successful when the configuration and backend state match", func(t *testing.T) { // Create a temporary working directory with state store configuration // that matches the backend state file @@ -3598,6 +3616,21 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { Meta: meta, } + // Before running init, confirm the contents of the backend state file before + statePath := filepath.Join(meta.DataDir(), DefaultStateFilename) + sMgr := &clistate.LocalState{Path: statePath} + if err := sMgr.RefreshState(); err != nil { + t.Fatal("Failed to load state:", err) + } + s := sMgr.State() + if s == nil { + t.Fatal("expected backend state file to be present, but there isn't one") + } + if diff := cmp.Diff(s.StateStore, expectedState); diff != "" { + t.Fatalf("unexpected diff in backend state file's description of state store:\n%s", diff) + } + + // Run init command args := []string{ "-enable-pluggable-state-storage-experiment=true", } @@ -3618,6 +3651,15 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { t.Fatalf("expected output to include %q, but got':\n %s", expected, output) } } + + // Confirm init was a no-op and backend state is unchanged afterwards + if err := sMgr.RefreshState(); err != nil { + t.Fatal("Failed to load state:", err) + } + s = sMgr.State() + if diff := cmp.Diff(s.StateStore, expectedState); diff != "" { + t.Fatalf("unexpected diff in backend state file's description of state store:\n%s", diff) + } }) } From 62394f8740ae03e2e7fe165e95835643c207f190 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 14:48:19 +0100 Subject: [PATCH 19/38] Remove unused fixture --- .../.terraform/terraform.tfstate | 20 ------------------- .../store-and-provider-config/main.tf | 14 ------------- 2 files changed, 34 deletions(-) delete mode 100644 internal/command/testdata/state-store-changed/store-and-provider-config/.terraform/terraform.tfstate delete mode 100644 internal/command/testdata/state-store-changed/store-and-provider-config/main.tf diff --git a/internal/command/testdata/state-store-changed/store-and-provider-config/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/store-and-provider-config/.terraform/terraform.tfstate deleted file mode 100644 index 2438ce6d2f8f..000000000000 --- a/internal/command/testdata/state-store-changed/store-and-provider-config/.terraform/terraform.tfstate +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 3, - "serial": 0, - "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", - "state_store": { - "type": "test_store", - "config": { - "value": "old-value" - }, - "provider": { - "version": "1.2.3", - "source": "registry.terraform.io/my-org/foo", - "config": { - "region": "old-value" - }, - "hash": 12345 - }, - "hash": 12345 - } -} \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/store-and-provider-config/main.tf b/internal/command/testdata/state-store-changed/store-and-provider-config/main.tf deleted file mode 100644 index 3202130af995..000000000000 --- a/internal/command/testdata/state-store-changed/store-and-provider-config/main.tf +++ /dev/null @@ -1,14 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } - state_store "test_store" { - provider "test" { - region = "changed-value" # changed versus backend state file - } - - value = "changed-value" # changed versus backend state file - } -} From 3ffe9a71724d02fecc13b87103bb70305c7c0554 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 15:40:04 +0100 Subject: [PATCH 20/38] Remove test that's replaced by new tests in command/init_test.go --- internal/command/meta_backend_test.go | 56 --------------------------- 1 file changed, 56 deletions(-) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 9002fd70c91a..59509c281eeb 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2125,62 +2125,6 @@ func TestMetaBackend_configuredStateStoreUnset(t *testing.T) { } } -// Changing a configured state store -// -// TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch -// case for this scenario, and will need to be updated when that init feature is implemented. -// ALSO, this test will need to be split into multiple scenarios in future. -func TestMetaBackend_changeConfiguredStateStore(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath("state-store-changed"), td) - t.Chdir(td) - - // Setup the meta - m := testMetaBackend(t, nil) - m.AllowExperimentalFeatures = true - - // Get the state store's config - mod, loadDiags := m.loadSingleModule(td) - if loadDiags.HasErrors() { - t.Fatalf("unexpected error when loading test config: %s", loadDiags.Err()) - } - - // Get mock provider to be used during init - // - // This imagines a provider called "test" that contains - // a pluggable state store implementation called "store". - mock := testStateStoreMock(t) - - // Define some locks to pass in - locks := depsfile.NewLocks() - providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") - constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") - if err != nil { - t.Fatalf("test setup failed when making constraint: %s", err) - } - locks.SetProvider( - providerAddr, - versions.MustParseVersion("9.9.9"), - constraint, - []providerreqs.Hash{""}, - ) - - // Get the operations backend - _, beDiags := m.Backend(&BackendOpts{ - Init: true, - StateStoreConfig: mod.StateStore, - ProviderFactory: providers.FactoryFixed(mock), - Locks: locks, - }) - if !beDiags.HasErrors() { - t.Fatal("expected an error to be returned during partial implementation of PSS") - } - wantErr := "Changing a state store configuration is not implemented yet" - if !strings.Contains(beDiags.Err().Error(), wantErr) { - t.Fatalf("expected the returned error to contain %q, but got: %s", wantErr, beDiags.Err()) - } -} - // Changing from using backend to state_store // // TODO(SarahFrench/radeksimko): currently this test only confirms that we're hitting the switch From 9d6464db41cf2a255cc367f29f634b0d7bb4fe54 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Wed, 15 Oct 2025 15:57:45 +0100 Subject: [PATCH 21/38] Replace old references to deleted "state-store-changed" test fixture & update test to not expect a value for region attr in provider config --- internal/command/meta_backend_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 59509c281eeb..1c75cbef8945 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2299,7 +2299,7 @@ func TestSavedStateStore(t *testing.T) { // Create a temporary working directory chunkSize := 42 td := t.TempDir() - testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file + testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) // Make a state manager for accessing the backend state file, @@ -2320,8 +2320,9 @@ func TestSavedStateStore(t *testing.T) { mock.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { // Assert that the state store is configured using backend state file values from the fixtures config := req.Config.AsValueMap() - if config["region"].AsString() != "old-value" { - t.Fatalf("expected the provider to be configured with values from the backend state file (the string \"old-value\"), not the config. Got: %#v", config) + if v, ok := config["region"]; ok && (v.Equals(cty.NullVal(cty.String)) != cty.True) { + // The backend state file has a null value for region, so if we're here we've somehow got a non-null value + t.Fatalf("expected the provider to be configured with values from the backend state file (where region is unset/null), not the config. Got value: %#v", v) } return providers.ConfigureProviderResponse{} } @@ -2389,7 +2390,7 @@ func TestSavedStateStore(t *testing.T) { t.Run("error - when there's no state stores in provider", func(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file + testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) // Make a state manager for accessing the backend state file, @@ -2421,7 +2422,7 @@ func TestSavedStateStore(t *testing.T) { t.Run("error - when there's no matching state store in provider Terraform suggests different identifier", func(t *testing.T) { // Create a temporary working directory td := t.TempDir() - testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file + testCopyDir(t, testFixturePath("state-store-changed/store-config"), td) // Fixtures with config that differs from backend state file t.Chdir(td) // Make a state manager for accessing the backend state file, From 74fd9b2f223f26f4048932b7b639e089c9f1701b Mon Sep 17 00:00:00 2001 From: Sarah French Date: Thu, 16 Oct 2025 11:12:11 +0100 Subject: [PATCH 22/38] Make test fixture coupling a little more understandable --- .../testdata/state-store-changed/provider-upgraded/main.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d8028e8c9918..6ca83e8d2ba4 100644 --- a/internal/command/testdata/state-store-changed/provider-upgraded/main.tf +++ b/internal/command/testdata/state-store-changed/provider-upgraded/main.tf @@ -2,7 +2,8 @@ terraform { required_providers { test = { source = "hashicorp/test" - # version = "9.9.9" // We've now specified using v9.9.9, versus the v1.2.3 used at last init and in the backend state file + # No version constraints here; we assume the test using this fixture forces the latest provider version + # to not match the backend state file in this folder. } } state_store "test_store" { From 9db12fda1b72fd451d7e4b7f3c9a00c1c163dbc3 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Thu, 16 Oct 2025 11:12:35 +0100 Subject: [PATCH 23/38] Refactor detection of no need to migrate into a function --- internal/command/meta_backend.go | 47 +++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index c2e67569b541..ddc531f5f905 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -952,18 +952,13 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // AND the same state_store implementation is used // AND the state_store config cache hash values match, indicating that the state_store config is valid and completely unchanged. // AND we're not providing any overrides. An override can mean a change overriding an unchanged state_store block (indicated by the hash value). - pVersion, vDiags := getStateStorageProviderVersion(stateStoreConfig, opts.Locks) - diags = diags.Append(vDiags) - if vDiags.HasErrors() { + + migrate, mDiags := stateStoreConfigNeedsMigration(s.StateStore, stateStoreConfig, uint64(stateStoreHash), uint64(stateStoreProviderHash), opts.Locks) + diags = diags.Append(mDiags) + if mDiags.HasErrors() { return nil, diags } - - if s.StateStore.Provider.Source.Equals(stateStoreConfig.ProviderAddr) && - s.StateStore.Provider.Version.Equal(pVersion) && - (uint64(stateStoreProviderHash) == s.StateStore.Provider.Hash) && - s.StateStore.Type == stateStoreConfig.Type && - (uint64(stateStoreHash) == s.StateStore.Hash) && - (!opts.Init || opts.ConfigOverride == nil) { + if !migrate && (!opts.Init || opts.ConfigOverride == nil) { log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q state_store configuration", stateStoreConfig.Type) savedStateStore, sssDiags := m.savedStateStore(sMgr, opts.ProviderFactory) diags = diags.Append(sssDiags) @@ -1003,6 +998,38 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } } +func stateStoreConfigNeedsMigration(s *workdir.StateStoreConfigState, storeConfig *configs.StateStore, stateStoreHash, stateStoreProviderHash uint64, locks *depsfile.Locks) (bool, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + pVersion, vDiags := getStateStorageProviderVersion(storeConfig, locks) + diags = diags.Append(vDiags) + if vDiags.HasErrors() { + return false, diags + } + + if !s.Provider.Source.Equals(storeConfig.ProviderAddr) { + log.Printf("[TRACE] Meta.Backend: state store provider addresses do not match") + return true, diags + } + if !s.Provider.Version.Equal(pVersion) { + log.Printf("[TRACE] Meta.Backend: state store provider version does not match") + return true, diags + } + if stateStoreProviderHash != s.Provider.Hash { + log.Printf("[TRACE] Meta.Backend: state store provider configuration has changed") + return true, diags + } + if s.Type != storeConfig.Type { + log.Printf("[TRACE] Meta.Backend: state store implementation has changed") + return true, diags + } + if stateStoreHash != s.Hash { + log.Printf("[TRACE] Meta.Backend: state store configuration has changed") + return true, diags + } + + return false, diags +} + // determineInitReason is used in non-Init commands to interrupt the command early and prompt users to instead run an init command. // That prompt needs to include the reason why init needs to be run, and it is determined here. // From 2e7c182a79bc9035645bc95bfc3cc0cad7c2b621 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Thu, 16 Oct 2025 11:13:03 +0100 Subject: [PATCH 24/38] Add TODO about more involved provider version change tests We will allow downgrades to succeed as long as the schema version number is unchanged --- internal/command/init_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 102f991e000c..deffc5a16171 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3967,6 +3967,9 @@ func TestInit_stateStore_configChanges(t *testing.T) { // Testing init's behaviors with `state_store` when the provider used for state storage in a previous init // command is updated. +// +// TODO: Add a test case showing that downgrading provider version is ok as long as the schema version hasn't +// changed. We should also have a test demonstrating that downgrades when the schema version HAS changed will fail. func TestInit_stateStore_providerUpgrade(t *testing.T) { t.Run("handling upgrading the provider used for state storage is currently unimplemented", func(t *testing.T) { // Create a temporary working directory with state store configuration From a68dd2be887679353339d032d72ced4feb66e944 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 13:04:11 +0100 Subject: [PATCH 25/38] Update (configs.StateStore)Hash method to return a single hash that's impacted by: state store config, provider config, state store type, provider source --- internal/configs/state_store.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/configs/state_store.go b/internal/configs/state_store.go index 25380244f052..cf6f290aef07 100644 --- a/internal/configs/state_store.go +++ b/internal/configs/state_store.go @@ -141,13 +141,14 @@ func resolveStateStoreProviderType(requiredProviders map[string]*RequiredProvide // for the purpose of hashing, so that an incomplete configuration can still // be hashed. Other errors, such as extraneous attributes, have no such special // case. -func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema *configschema.Block) (stateStoreHash, providerHash int, diags tfdiags.Diagnostics) { +func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema *configschema.Block) (int, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics // 1. Prepare the state_store hash // The state store schema should not include a provider block or attr if _, exists := stateStoreSchema.Attributes["provider"]; exists { - return 0, 0, diags.Append(&hcl.Diagnostic{ + return 0, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Protected argument name \"provider\" in state store schema", Detail: "Schemas for state stores cannot contain attributes or blocks called \"provider\", to avoid confusion with the provider block nested inside the state_store block. This is a bug in the provider used for state storage, which should be reported in the provider's own issue tracker.", @@ -155,7 +156,7 @@ func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema * }) } if _, exists := stateStoreSchema.BlockTypes["provider"]; exists { - return 0, 0, diags.Append(&hcl.Diagnostic{ + return 0, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Protected block name \"provider\" in state store schema", Detail: "Schemas for state stores cannot contain attributes or blocks called \"provider\", to avoid confusion with the provider block nested inside the state_store block. This is a bug in the provider used for state storage, which should be reported in the provider's own issue tracker.", @@ -178,18 +179,14 @@ func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema * diags = diags.Append(diag) } if diags.HasErrors() { - return 0, 0, diags + return 0, diags } } - // We're on the happy path, so continue to get the hash + // We're on the happy path, but handle if we got a nil value above if ssVal == cty.NilVal { ssVal = cty.UnknownVal(schema.ImpliedType()) } - ssToHash := cty.TupleVal([]cty.Value{ - cty.StringVal(b.Type), - ssVal, - }) // 2. Prepare the provider hash schema = providerSchema.NoneRequired() @@ -197,15 +194,18 @@ func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema * pVal, decodeDiags := hcldec.Decode(b.Provider.Config, spec, nil) if decodeDiags.HasErrors() { diags = diags.Append(decodeDiags) - return 0, 0, diags + return 0, diags } if pVal == cty.NilVal { pVal = cty.UnknownVal(schema.ImpliedType()) } - pToHash := cty.TupleVal([]cty.Value{ - cty.StringVal(b.Type), - pVal, - }) - return ssToHash.Hash(), pToHash.Hash(), diags + toHash := cty.TupleVal([]cty.Value{ + cty.StringVal(b.Type), // state store type + ssVal, // state store config + + cty.StringVal(b.ProviderAddr.String()), // provider source + pVal, // provider config + }) + return toHash.Hash(), diags } From 07c4ce28f3806c162f64264833b37d8284021b25 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 13:05:56 +0100 Subject: [PATCH 26/38] Update calling code and test helper code to reflect that the nested provider block no longer has its own hash --- internal/command/meta_backend.go | 66 +++++++------------ .../workdir/statestore_config_state.go | 4 +- internal/command/workdir/testing.go | 1 - 3 files changed, 25 insertions(+), 46 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index ddc531f5f905..ab3cc8486f32 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -568,7 +568,7 @@ func (m *Meta) backendConfig(opts *BackendOpts) (*configs.Backend, int, tfdiags. // > Ensures that that state store type exists in the linked provider. // > Returns config that is the combination of config and any config overrides originally supplied via the CLI. // > Returns a hash of the config in the configuration files, i.e. excluding overrides -func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, int, tfdiags.Diagnostics) { +func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics c := opts.StateStoreConfig @@ -580,7 +580,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in Summary: "Missing state store configuration", Detail: "Terraform attempted to configure a state store when no parsed 'state_store' configuration was present. This is a bug in Terraform and should be reported.", }) - return nil, 0, 0, diags + return nil, 0, diags } // Check - is the state store type in the config supported by the provider? @@ -590,12 +590,12 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in Summary: "Missing provider details when configuring state store", Detail: "Terraform attempted to configure a state store and no provider factory was available to launch it. This is a bug in Terraform and should be reported.", }) - return nil, 0, 0, diags + return nil, 0, diags } provider, err := opts.ProviderFactory() if err != nil { diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err)) - return nil, 0, 0, diags + return nil, 0, diags } defer provider.Close() // Stop the child process once we're done with it here. @@ -610,7 +610,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in c.ProviderAddr), Subject: &c.DeclRange, }) - return nil, 0, 0, diags + return nil, 0, diags } stateStoreSchema, exists := resp.StateStores[c.Type] @@ -628,7 +628,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in c.ProviderAddr, suggestion), Subject: &c.DeclRange, }) - return nil, 0, 0, diags + return nil, 0, diags } // We know that the provider contains a state store with the correct type name. @@ -638,7 +638,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in // > Apply any overrides configBody := c.Config - stateStoreHash, providerHash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body) + hash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body) // If we have an override configuration body then we must apply it now. if opts.ConfigOverride != nil { @@ -646,13 +646,13 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, in configBody = configs.MergeBodies(configBody, opts.ConfigOverride) } - log.Printf("[TRACE] Meta.Backend: built configuration for %q state_store with hash value %d and nested provider block with hash value %d", c.Type, stateStoreHash, providerHash) + log.Printf("[TRACE] Meta.Backend: built configuration for %q state_store with hash value %d", c.Type, hash) // We'll shallow-copy configs.StateStore here so that we can replace the // body without affecting others that hold this reference. configCopy := *c configCopy.Config = configBody - return &configCopy, stateStoreHash, providerHash, diags + return &configCopy, hash, diags } // backendFromConfig returns the initialized (not configured) backend @@ -673,13 +673,11 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // Get the local 'backend' or 'state_store' configuration. var backendConfig *configs.Backend var stateStoreConfig *configs.StateStore - var backendHash int - var stateStoreHash int - var stateStoreProviderHash int + var cHash int if opts.StateStoreConfig != nil { // state store has been parsed from config and is included in opts var ssDiags tfdiags.Diagnostics - stateStoreConfig, stateStoreHash, stateStoreProviderHash, ssDiags = m.stateStoreConfig(opts) + stateStoreConfig, cHash, ssDiags = m.stateStoreConfig(opts) diags = diags.Append(ssDiags) if ssDiags.HasErrors() { return nil, diags @@ -688,7 +686,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // backend config may or may not have been parsed and included in opts, // or may not exist in config at all (default/implied local backend) var beDiags tfdiags.Diagnostics - backendConfig, backendHash, beDiags = m.backendConfig(opts) + backendConfig, cHash, beDiags = m.backendConfig(opts) diags = diags.Append(beDiags) if beDiags.HasErrors() { return nil, diags @@ -779,7 +777,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return nil, diags } - return m.backend_c_r_S(backendConfig, backendHash, sMgr, true, opts) + return m.backend_c_r_S(backendConfig, cHash, sMgr, true, opts) // We're unsetting a state_store (moving from state_store => local) case stateStoreConfig == nil && !s.StateStore.Empty() && @@ -809,7 +807,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } return nil, diags } - return m.backend_C_r_s(backendConfig, backendHash, sMgr, opts) + return m.backend_C_r_s(backendConfig, cHash, sMgr, opts) // Configuring a state store for the first time or -reconfigure flag was used case stateStoreConfig != nil && s.StateStore.Empty() && @@ -830,7 +828,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return nil, diags } - return m.stateStore_C_s(stateStoreConfig, stateStoreHash, stateStoreProviderHash, sMgr, opts) + return m.stateStore_C_s(stateStoreConfig, cHash, sMgr, opts) // Migration from state store to backend case backendConfig != nil && s.Backend.Empty() && @@ -870,7 +868,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // We're not initializing // AND the backend cache hash values match, indicating that the stored config is valid and completely unchanged. // AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value). - if (uint64(backendHash) == s.Backend.Hash) && (!opts.Init || opts.ConfigOverride == nil) { + if (uint64(cHash) == s.Backend.Hash) && (!opts.Init || opts.ConfigOverride == nil) { log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q backend configuration", backendConfig.Type) savedBackend, diags := m.savedBackend(sMgr) // Verify that selected workspace exist. Otherwise prompt user to create one @@ -898,7 +896,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // It's possible for a backend to be unchanged, and the config itself to // have changed by moving a parameter from the config to `-backend-config` // In this case, we update the Hash. - moreDiags = m.updateSavedBackendHash(backendHash, sMgr) + moreDiags = m.updateSavedBackendHash(cHash, sMgr) if moreDiags.HasErrors() { return nil, diags } @@ -929,7 +927,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } log.Printf("[WARN] backend config has changed since last init") - return m.backend_C_r_S_changed(backendConfig, backendHash, sMgr, true, opts) + return m.backend_C_r_S_changed(backendConfig, cHash, sMgr, true, opts) // Potentially changing a state store configuration case backendConfig == nil && s.Backend.Empty() && @@ -944,21 +942,10 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // > Changing how the store is configured. // > Allowing values to be moved between partial overrides and config - // We are not going to migrate if... - // - // The state storage provider is the same - // AND the provider version is the same - // AND the provider config cache hash values match, indicating that the provider config is valid and completely unchanged. - // AND the same state_store implementation is used - // AND the state_store config cache hash values match, indicating that the state_store config is valid and completely unchanged. - // AND we're not providing any overrides. An override can mean a change overriding an unchanged state_store block (indicated by the hash value). - - migrate, mDiags := stateStoreConfigNeedsMigration(s.StateStore, stateStoreConfig, uint64(stateStoreHash), uint64(stateStoreProviderHash), opts.Locks) - diags = diags.Append(mDiags) - if mDiags.HasErrors() { - return nil, diags - } - if !migrate && (!opts.Init || opts.ConfigOverride == nil) { + // We're not initializing + // AND the config's and backend state file's hash values match, indicating that the stored config is valid and completely unchanged. + // AND we're not providing any overrides. An override can mean a change overriding an unchanged backend block (indicated by the hash value). + if (uint64(cHash) == s.StateStore.Hash) && (!opts.Init || opts.ConfigOverride == nil) { log.Printf("[TRACE] Meta.Backend: using already-initialized, unchanged %q state_store configuration", stateStoreConfig.Type) savedStateStore, sssDiags := m.savedStateStore(sMgr, opts.ProviderFactory) diags = diags.Append(sssDiags) @@ -998,7 +985,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } } -func stateStoreConfigNeedsMigration(s *workdir.StateStoreConfigState, storeConfig *configs.StateStore, stateStoreHash, stateStoreProviderHash uint64, locks *depsfile.Locks) (bool, tfdiags.Diagnostics) { +func stateStoreConfigNeedsMigration(s *workdir.StateStoreConfigState, storeConfig *configs.StateStore, stateStoreHash uint64, locks *depsfile.Locks) (bool, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics pVersion, vDiags := getStateStorageProviderVersion(storeConfig, locks) diags = diags.Append(vDiags) @@ -1014,10 +1001,6 @@ func stateStoreConfigNeedsMigration(s *workdir.StateStoreConfigState, storeConfi log.Printf("[TRACE] Meta.Backend: state store provider version does not match") return true, diags } - if stateStoreProviderHash != s.Provider.Hash { - log.Printf("[TRACE] Meta.Backend: state store provider configuration has changed") - return true, diags - } if s.Type != storeConfig.Type { log.Printf("[TRACE] Meta.Backend: state store implementation has changed") return true, diags @@ -1632,7 +1615,7 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi //------------------------------------------------------------------- // Configuring a state_store for the first time. -func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, providerHash int, backendSMgr *clistate.LocalState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backendSMgr *clistate.LocalState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics vt := arguments.ViewJSON @@ -1769,7 +1752,6 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, provide Provider: &workdir.ProviderConfigState{ Source: &c.ProviderAddr, Version: pVersion, - Hash: uint64(providerHash), }, } s.StateStore.SetConfig(storeConfigVal, b.ConfigSchema()) diff --git a/internal/command/workdir/statestore_config_state.go b/internal/command/workdir/statestore_config_state.go index 2833f28f3002..020fe1a0324f 100644 --- a/internal/command/workdir/statestore_config_state.go +++ b/internal/command/workdir/statestore_config_state.go @@ -27,7 +27,7 @@ type StateStoreConfigState struct { Type string `json:"type"` // State store type name Provider *ProviderConfigState `json:"provider"` // Details about the state-storage provider ConfigRaw json.RawMessage `json:"config"` // state_store block raw config, barring provider details - Hash uint64 `json:"hash"` // Hash of the state_store block's configuration, excluding the provider block and any values supplied via methods other than config + Hash uint64 `json:"hash"` // Hash of the state_store block's configuration, including the nested provider block } // Empty returns true if there is no active state store. @@ -139,7 +139,6 @@ func (s *StateStoreConfigState) DeepCopy() *StateStoreConfigState { provider := &ProviderConfigState{ Version: s.Provider.Version, Source: s.Provider.Source, - Hash: s.Provider.Hash, } if s.Provider.ConfigRaw != nil { provider.ConfigRaw = make([]byte, len(s.Provider.ConfigRaw)) @@ -168,7 +167,6 @@ type ProviderConfigState struct { Version *version.Version `json:"version"` // The specific provider version used for the state store. Should be set using a getproviders.Version, etc. Source *tfaddr.Provider `json:"source"` // The FQN/fully-qualified name of the provider. ConfigRaw json.RawMessage `json:"config"` // state_store block raw config, barring provider details - Hash uint64 `json:"hash"` // Hash of the nested provider block's configuration, excluding any values supplied via methods other than config } // Empty returns true if there is no provider config state data. diff --git a/internal/command/workdir/testing.go b/internal/command/workdir/testing.go index c86859a2be82..0c449322d2b8 100644 --- a/internal/command/workdir/testing.go +++ b/internal/command/workdir/testing.go @@ -37,6 +37,5 @@ func getTestProviderState(t *testing.T, semVer, hostname, namespace, typeName, c Type: typeName, }, ConfigRaw: []byte(config), - Hash: 12345, } } From 4cb47733791b1244beb55c15fae09ebb537529a9 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 13:06:40 +0100 Subject: [PATCH 27/38] Remove test; there is now a single hash that SHOULD be affected by the provider block! --- internal/configs/state_store_test.go | 62 ---------------------------- 1 file changed, 62 deletions(-) diff --git a/internal/configs/state_store_test.go b/internal/configs/state_store_test.go index 266c5e56a072..0ee727b85386 100644 --- a/internal/configs/state_store_test.go +++ b/internal/configs/state_store_test.go @@ -208,68 +208,6 @@ func TestStateStore_Hash(t *testing.T) { } } -func TestStateStore_checkStateStoreHashUnaffectedByProviderBlock(t *testing.T) { - - // Normally these schemas would come from a provider's GetProviderSchema data - stateStoreSchema := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "path": { - Type: cty.String, - Required: true, - }, - "workspace_dir": { - Type: cty.String, - Optional: true, - }, - }, - } - providerSchema := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foobar": { - Type: cty.String, - Required: true, - }, - }, - } - providerConfig := configBodyForTest(t, `foobar = "foobar"`) - - // Make two StateStores: - // 1) Has provider block in the main Config value, as well as matching data in Provider.Config - // 2) Doesn't have provider block in the main Config value, has config in Provider.Config - - s1 := StateStore{ - Config: configBodyForTest(t, `state_store "foo" { - provider "foobar" { - foobar = "foobar" - } - path = "mystate.tfstate" - workspace_dir = "foobar" - }`), - Provider: &Provider{ - Config: providerConfig, - }, - } - s2 := StateStore{ - Config: configBodyForTest(t, `state_store "foo" { - # No provider block here - - path = "mystate.tfstate" - workspace_dir = "foobar" - }`), - Provider: &Provider{ - Config: providerConfig, - }, - } - - s1StoreHash, _, _ := s1.Hash(stateStoreSchema, providerSchema) - s2StoreHash, _, _ := s2.Hash(stateStoreSchema, providerSchema) - - if s1StoreHash != s2StoreHash { - t.Fatalf("expected state_store block hashes to match, as hashing logic should ignore presence of provider block. Got s1 %d, s2 %d", s1StoreHash, s2StoreHash) - } - -} - func configBodyForTest(t *testing.T, config string) hcl.Body { t.Helper() f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1}) From 25e4dc219c59a5dfa923dfdcde7dfa7d01a816b6 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 13:31:12 +0100 Subject: [PATCH 28/38] Also use provider name, from config, in hash --- internal/configs/state_store.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/configs/state_store.go b/internal/configs/state_store.go index cf6f290aef07..82d072fa1d49 100644 --- a/internal/configs/state_store.go +++ b/internal/configs/state_store.go @@ -204,6 +204,7 @@ func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema * cty.StringVal(b.Type), // state store type ssVal, // state store config + cty.StringVal(b.Provider.Name), // provider name - this reflects the config, whereas provider source is influenced by config but separate cty.StringVal(b.ProviderAddr.String()), // provider source pVal, // provider config }) From 5b116317d350ef9ade0546d14337b4ca96ded804 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 14:29:34 +0100 Subject: [PATCH 29/38] Update tests to reflect changes in how hashes are made --- internal/command/meta_backend_test.go | 10 +- .../command/workdir/backend_state_test.go | 6 +- internal/configs/state_store_test.go | 120 ++++++++++++------ 3 files changed, 87 insertions(+), 49 deletions(-) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 1c75cbef8945..7ce96dd5d6b9 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2696,7 +2696,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { } m := testMetaBackend(t, nil) - finalConfig, _, _, diags := m.stateStoreConfig(opts) + finalConfig, _, diags := m.stateStoreConfig(opts) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } @@ -2723,7 +2723,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { } m := testMetaBackend(t, nil) - _, _, _, diags := m.stateStoreConfig(opts) + _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2744,7 +2744,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { } m := testMetaBackend(t, nil) - _, _, _, diags := m.stateStoreConfig(opts) + _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2768,7 +2768,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { } m := testMetaBackend(t, nil) - _, _, _, diags := m.stateStoreConfig(opts) + _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } @@ -2795,7 +2795,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { } m := testMetaBackend(t, nil) - _, _, _, diags := m.stateStoreConfig(opts) + _, _, diags := m.stateStoreConfig(opts) if !diags.HasErrors() { t.Fatal("expected errors but got none") } diff --git a/internal/command/workdir/backend_state_test.go b/internal/command/workdir/backend_state_test.go index 1075154fd72c..8ac4905b1179 100644 --- a/internal/command/workdir/backend_state_test.go +++ b/internal/command/workdir/backend_state_test.go @@ -179,7 +179,7 @@ func TestEncodeBackendStateFile(t *testing.T) { Hash: 123, }, }, - Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": \"1.2.3\",\n \"source\": \"registry.terraform.io/my-org/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 12345\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), + Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": \"1.2.3\",\n \"source\": \"registry.terraform.io/my-org/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), }, "it's valid to record no version data when a builtin provider used for state store": { Input: &BackendStateFile{ @@ -190,7 +190,7 @@ func TestEncodeBackendStateFile(t *testing.T) { Hash: 123, }, }, - Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"terraform.io/builtin/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 12345\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), + Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"terraform.io/builtin/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), }, "it's valid to record no version data when a re-attached provider used for state store": { Input: &BackendStateFile{ @@ -215,7 +215,7 @@ func TestEncodeBackendStateFile(t *testing.T) { } }`, }, - Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"registry.terraform.io/hashicorp/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 12345\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), + Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"registry.terraform.io/hashicorp/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), }, "error when neither backend nor state_store config state are present": { Input: &BackendStateFile{}, diff --git a/internal/configs/state_store_test.go b/internal/configs/state_store_test.go index 0ee727b85386..91076f718f0c 100644 --- a/internal/configs/state_store_test.go +++ b/internal/configs/state_store_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/zclconf/go-cty/cty" ) @@ -59,43 +60,65 @@ func TestStateStore_Hash(t *testing.T) { }, } + // These two values are coupled. + exampleConfig := configBodyForTest(t, `state_store "foobar_fs" { + provider "foobar" { + foobar = "foobar" + } + path = "mystate.tfstate" + workspace_dir = "foobar" + }`) + exampleHash := 33464751 + cases := map[string]struct { - config hcl.Body - providerConfig hcl.Body - schema *configschema.Block - wantErrorString string - wantProviderHash int - wantStateStoreHash int + config hcl.Body + stateStoreSchema *configschema.Block + providerAddr tfaddr.Provider + wantErrorString string + wantHash int }{ - "ignores the provider block in config data, as long as the schema doesn't include it": { - schema: stateStoreSchema, - config: configBodyForTest(t, `state_store "foo" { + "example happy path with all attrs set in the configuration": { + stateStoreSchema: stateStoreSchema, + config: exampleConfig, + wantHash: exampleHash, + }, + "changing the state store type affects the hash value": { + stateStoreSchema: stateStoreSchema, + config: configBodyForTest(t, `state_store "foobar_CHANGED_VALUE_HERE" { provider "foobar" { - foobar = "foobar" + foobar = "foobar" + } + path = "mystate.tfstate" + workspace_dir = "foobar" + }`), + wantHash: 559959421, // Differs from `exampleHash` + }, + "changing the provider affects the hash value": { + stateStoreSchema: stateStoreSchema, + providerAddr: tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "different-provider"), + config: configBodyForTest(t, `state_store "different-provider_fs" { + provider "different-provider" { + foobar = "foobar" } path = "mystate.tfstate" workspace_dir = "foobar" }`), - providerConfig: configBodyForTest(t, `foobar = "foobar"`), - wantProviderHash: 2672365208, - wantStateStoreHash: 3037430836, + wantHash: 1672894798, // Differs from `exampleHash` }, "tolerates empty config block for the provider even when schema has Required field(s)": { - schema: stateStoreSchema, - config: configBodyForTest(t, `state_store "foo" { + stateStoreSchema: stateStoreSchema, + config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { # required field "foobar" is missing } path = "mystate.tfstate" workspace_dir = "foobar" }`), - providerConfig: hcl.EmptyBody(), - wantProviderHash: 2911589008, - wantStateStoreHash: 3037430836, + wantHash: 3558227459, }, "tolerates missing Required field(s) in state_store config": { - schema: stateStoreSchema, - config: configBodyForTest(t, `state_store "foo" { + stateStoreSchema: stateStoreSchema, + config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" } @@ -103,13 +126,11 @@ func TestStateStore_Hash(t *testing.T) { # required field "path" is missing workspace_dir = "foobar" }`), - providerConfig: configBodyForTest(t, `foobar = "foobar"`), - wantProviderHash: 2672365208, - wantStateStoreHash: 3453024478, + wantHash: 3682853451, }, - "returns errors when the config contains non-provider things that aren't in the schema": { - schema: stateStoreSchema, - config: configBodyForTest(t, `state_store "foo" { + "returns errors when the state_store config doesn't match the schema": { + stateStoreSchema: stateStoreSchema, + config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" } @@ -120,11 +141,25 @@ func TestStateStore_Hash(t *testing.T) { path = "mystate.tfstate" workspace_dir = "foobar" }`), - providerConfig: configBodyForTest(t, `foobar = "foobar"`), wantErrorString: "Unsupported argument", }, - "returns an error if the schema includes a provider block": { - schema: &configschema.Block{ + "returns errors when the provider config doesn't match the schema": { + stateStoreSchema: stateStoreSchema, + config: configBodyForTest(t, `state_store "foobar_fs" { + provider "foobar" { + foobar = "foobar" + unexpected_attr = "foobar" + unexpected_block { + foobar = "foobar" + } + } + path = "mystate.tfstate" + workspace_dir = "foobar" + }`), + wantErrorString: "Unsupported argument", + }, + "returns an error if the state_store schema includes a provider block": { + stateStoreSchema: &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ "provider": { Block: configschema.Block{ @@ -139,18 +174,17 @@ func TestStateStore_Hash(t *testing.T) { }, }, }, - config: configBodyForTest(t, `state_store "foo" { + config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" } path = "mystate.tfstate" workspace_dir = "foobar" }`), - providerConfig: configBodyForTest(t, `foobar = "foobar"`), wantErrorString: `Protected block name "provider" in state store schema`, }, - "returns an error if the schema includes a provider attribute": { - schema: &configschema.Block{ + "returns an error if the state_store schema includes a provider attribute": { + stateStoreSchema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "provider": { Type: cty.String, @@ -158,20 +192,20 @@ func TestStateStore_Hash(t *testing.T) { }, }, }, - config: configBodyForTest(t, `state_store "foo" { + config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" } path = "mystate.tfstate" workspace_dir = "foobar" }`), - providerConfig: configBodyForTest(t, `foobar = "foobar"`), wantErrorString: `Protected argument name "provider" in state store schema`, }, } for tn, tc := range cases { t.Run(tn, func(t *testing.T) { + // Construct a configs.StateStore for the test. content, _, cfgDiags := tc.config.PartialContent(terraformBlockSchema) if len(cfgDiags) > 0 { t.Fatalf("unexpected diagnostics: %s", cfgDiags) @@ -181,8 +215,15 @@ func TestStateStore_Hash(t *testing.T) { if len(ssDiags) > 0 { t.Fatalf("unexpected diagnostics: %s", ssDiags) } + // Add provider addr + if tc.providerAddr.IsZero() { + s.ProviderAddr = tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") + } else { + s.ProviderAddr = tc.providerAddr + } - ssHash, pHash, diags := s.Hash(tc.schema, providerSchema) + // Test Hash method. + gotHash, diags := s.Hash(tc.stateStoreSchema, providerSchema) if diags.HasErrors() { if tc.wantErrorString == "" { t.Fatalf("unexpected error: %s", diags.Err()) @@ -198,11 +239,8 @@ func TestStateStore_Hash(t *testing.T) { t.Fatal("expected an error when generating a hash, but got none") } - if ssHash != tc.wantStateStoreHash { - t.Fatalf("expected hash for state_store to be %d, but got %d", tc.wantStateStoreHash, ssHash) - } - if pHash != tc.wantProviderHash { - t.Fatalf("expected hash for provider to be %d, but got %d", tc.wantProviderHash, pHash) + if gotHash != tc.wantHash { + t.Fatalf("expected hash for state_store to be %d, but got %d", tc.wantHash, gotHash) } }) } From 9d1c54a5ad803001a3b904e35235d79a2b240c22 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 14:31:38 +0100 Subject: [PATCH 30/38] Remove unused `stateStoreConfigNeedsMigration` function --- internal/command/meta_backend.go | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index ab3cc8486f32..e78764ff6311 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -985,34 +985,6 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } } -func stateStoreConfigNeedsMigration(s *workdir.StateStoreConfigState, storeConfig *configs.StateStore, stateStoreHash uint64, locks *depsfile.Locks) (bool, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - pVersion, vDiags := getStateStorageProviderVersion(storeConfig, locks) - diags = diags.Append(vDiags) - if vDiags.HasErrors() { - return false, diags - } - - if !s.Provider.Source.Equals(storeConfig.ProviderAddr) { - log.Printf("[TRACE] Meta.Backend: state store provider addresses do not match") - return true, diags - } - if !s.Provider.Version.Equal(pVersion) { - log.Printf("[TRACE] Meta.Backend: state store provider version does not match") - return true, diags - } - if s.Type != storeConfig.Type { - log.Printf("[TRACE] Meta.Backend: state store implementation has changed") - return true, diags - } - if stateStoreHash != s.Hash { - log.Printf("[TRACE] Meta.Backend: state store configuration has changed") - return true, diags - } - - return false, diags -} - // determineInitReason is used in non-Init commands to interrupt the command early and prompt users to instead run an init command. // That prompt needs to include the reason why init needs to be run, and it is determined here. // From 63ce4e74cc356441f51159bbc15c75578954ebc3 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 14:36:21 +0100 Subject: [PATCH 31/38] Remove duplicate isProviderReattached function. --- internal/command/meta_backend.go | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index e78764ff6311..f64ebb3cb654 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1807,7 +1807,7 @@ func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks var pVersion *version.Version isBuiltin := c.ProviderAddr.Hostname == addrs.BuiltInProviderHost - isReattached, err := isProviderReattached(c.ProviderAddr) + isReattached, err := reattach.IsProviderReattached(c.ProviderAddr, os.Getenv("TF_REATTACH_PROVIDERS")) if err != nil { diags = diags.Append(err) return nil, diags @@ -1837,29 +1837,6 @@ func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks return pVersion, diags } -// isProviderReattached determines if a given provider is being supplied to Terraform via the TF_REATTACH_PROVIDERS -// environment variable. -func isProviderReattached(provider addrs.Provider) (bool, error) { - in := os.Getenv("TF_REATTACH_PROVIDERS") - if in != "" { - var m map[string]any - err := json.Unmarshal([]byte(in), &m) - if err != nil { - return false, fmt.Errorf("Invalid format for TF_REATTACH_PROVIDERS: %w", err) - } - for p := range m { - a, diags := addrs.ParseProviderSourceString(p) - if diags.HasErrors() { - return false, fmt.Errorf("Error parsing %q as a provider address: %w", a, diags.Err()) - } - if a.Equals(provider) { - return true, nil - } - } - } - return false, nil -} - // 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. From f2cbb3b4a907f38bbe8197fd8f9026848919e63d Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 15:50:57 +0100 Subject: [PATCH 32/38] Fixes to affected tests --- internal/command/init_test.go | 3 --- internal/command/workdir/backend_state_test.go | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index deffc5a16171..07c4baecfba9 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3313,7 +3313,6 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { Type: "test", }, ConfigRaw: []byte("{\n \"region\": null\n }"), - Hash: uint64(3976463117), // Hash of empty config }, } if diff := cmp.Diff(s.StateStore, expectedState); diff != "" { @@ -3575,7 +3574,6 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { Type: "test", }, ConfigRaw: []byte("{\n \"region\": null\n }"), - Hash: uint64(3976463117), // Hash of empty config }, } @@ -3747,7 +3745,6 @@ func TestInit_stateStore_configChanges(t *testing.T) { Type: "test", }, ConfigRaw: []byte("{\n \"region\": null\n }"), - Hash: uint64(3976463117), // Hash of empty config }, } if diff := cmp.Diff(s.StateStore, expectedState); diff != "" { diff --git a/internal/command/workdir/backend_state_test.go b/internal/command/workdir/backend_state_test.go index 8ac4905b1179..1937000bf5cc 100644 --- a/internal/command/workdir/backend_state_test.go +++ b/internal/command/workdir/backend_state_test.go @@ -179,7 +179,7 @@ func TestEncodeBackendStateFile(t *testing.T) { Hash: 123, }, }, - Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": \"1.2.3\",\n \"source\": \"registry.terraform.io/my-org/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), + Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": \"1.2.3\",\n \"source\": \"registry.terraform.io/my-org/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n }\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), }, "it's valid to record no version data when a builtin provider used for state store": { Input: &BackendStateFile{ @@ -190,7 +190,7 @@ func TestEncodeBackendStateFile(t *testing.T) { Hash: 123, }, }, - Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"terraform.io/builtin/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), + Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"terraform.io/builtin/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n }\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), }, "it's valid to record no version data when a re-attached provider used for state store": { Input: &BackendStateFile{ @@ -215,7 +215,7 @@ func TestEncodeBackendStateFile(t *testing.T) { } }`, }, - Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"registry.terraform.io/hashicorp/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n },\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), + Want: []byte("{\n \"version\": 3,\n \"terraform_version\": \"" + tfVersion + "\",\n \"state_store\": {\n \"type\": \"foobar_baz\",\n \"provider\": {\n \"version\": null,\n \"source\": \"registry.terraform.io/hashicorp/foobar\",\n \"config\": {\n \"foo\": \"bar\"\n }\n },\n \"config\": {\n \"foo\": \"bar\"\n },\n \"hash\": 123\n }\n}"), }, "error when neither backend nor state_store config state are present": { Input: &BackendStateFile{}, From 647bca77e32dcbfe6cc597260ecf461a8d1140f2 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 17:43:10 +0100 Subject: [PATCH 33/38] Allow provider version to impact the state storage hash, update impacted tests and test fixtures --- internal/command/init_test.go | 6 +- internal/command/meta_backend.go | 17 +- .../.terraform/terraform.tfstate | 5 +- .../.terraform/terraform.tfstate | 5 +- .../.terraform/terraform.tfstate | 5 +- .../.terraform/terraform.tfstate | 5 +- .../store-config/.terraform/terraform.tfstate | 5 +- .../.terraform/terraform.tfstate | 5 +- internal/configs/state_store.go | 10 +- internal/configs/state_store_test.go | 196 ++++++++++++------ 10 files changed, 166 insertions(+), 93 deletions(-) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 07c4baecfba9..91a6ffe51d27 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -3304,7 +3304,7 @@ func TestInit_stateStore_newWorkingDir(t *testing.T) { expectedState := &workdir.StateStoreConfigState{ Type: "test_store", ConfigRaw: []byte("{\n \"value\": \"foobar\"\n }"), - Hash: uint64(2116468040), // Hash affected by config + Hash: uint64(4158988729), Provider: &workdir.ProviderConfigState{ Version: v1_2_3, Source: &tfaddr.Provider{ @@ -3565,7 +3565,7 @@ func TestInit_stateStore_configUnchanged(t *testing.T) { expectedState := &workdir.StateStoreConfigState{ Type: "test_store", ConfigRaw: []byte("{\n \"value\": \"foobar\"\n }"), - Hash: uint64(2116468040), // Hash affected by config + Hash: uint64(4158988729), Provider: &workdir.ProviderConfigState{ Version: v1_2_3, Source: &tfaddr.Provider{ @@ -3736,7 +3736,7 @@ func TestInit_stateStore_configChanges(t *testing.T) { expectedState := &workdir.StateStoreConfigState{ Type: "test_store", ConfigRaw: []byte("{\n \"value\": \"changed-value\"\n }"), - Hash: uint64(1417640992), // Hash affected by config + Hash: uint64(1157855489), // The new hash after reconfiguring; this doesn't match the backend state test fixture Provider: &workdir.ProviderConfigState{ Version: v1_2_3, Source: &tfaddr.Provider{ diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index f64ebb3cb654..46241932b09f 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -583,6 +583,14 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, tf return nil, 0, diags } + // Get the provider version from locks, as this impacts the hash + // NOTE: this assumes that we will never allow users to override config definint which provider is used for state storage + stateStoreProviderVersion, vDiags := getStateStorageProviderVersion(opts.StateStoreConfig, opts.Locks) + diags = diags.Append(vDiags) + if vDiags.HasErrors() { + return nil, 0, diags + } + // Check - is the state store type in the config supported by the provider? if opts.ProviderFactory == nil { diags = diags.Append(&hcl.Diagnostic{ @@ -638,7 +646,7 @@ func (m *Meta) stateStoreConfig(opts *BackendOpts) (*configs.StateStore, int, tf // > Apply any overrides configBody := c.Config - hash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body) + hash, diags := c.Hash(stateStoreSchema.Body, resp.Provider.Body, stateStoreProviderVersion) // If we have an override configuration body then we must apply it now. if opts.ConfigOverride != nil { @@ -1800,7 +1808,10 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend return b, diags } -// getStateStorageProviderVersion assumes that calling code has checked whether the provider is fully managed by Terraform, +// getStateStorageProviderVersion gets the current version of the state store provider that's in use. This is achieved +// by inspecting the current locks. +// +// This function assumes that calling code has checked whether the provider is fully managed by Terraform, // or is built-in, before using this method and is prepared to receive a nil Version. func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks) (*version.Version, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -1826,7 +1837,7 @@ func getStateStorageProviderVersion(c *configs.StateStore, locks *depsfile.Locks } pVersion, err = providerreqs.GoVersionFromVersion(pLock.Version()) if err != nil { - diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) when recording backend state for state store %q. This is a bug in Terraform and should be reported: %w", + diags = diags.Append(fmt.Errorf("Failed obtain the in-use version of provider %s (%q) used with state store %q. This is a bug in Terraform and should be reported: %w", c.Provider.Name, c.ProviderAddr, c.Type, diff --git a/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate index 1a09f5af080d..4f96aa73ee7d 100644 --- a/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/provider-config/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": null - }, - "hash": 3976463117 + } }, - "hash": 2116468040 + "hash": 4158988729 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate index f875064036af..9bdea48296ac 100644 --- a/internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/provider-upgraded/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": "foobar" - }, - "hash": 2116468040 + } }, - "hash": 2116468040 + "hash": 3395824466 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate index f875064036af..9bdea48296ac 100644 --- a/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/provider-used/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": "foobar" - }, - "hash": 2116468040 + } }, - "hash": 2116468040 + "hash": 3395824466 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate index f875064036af..9bdea48296ac 100644 --- a/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/state-store-type/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": "foobar" - }, - "hash": 2116468040 + } }, - "hash": 2116468040 + "hash": 3395824466 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate index a2292b07b998..91ccd5b4350f 100644 --- a/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-changed/store-config/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": null - }, - "hash": 3976463117 + } }, - "hash": 2116468040 + "hash": 1505635192 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate index 1a09f5af080d..4f96aa73ee7d 100644 --- a/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-unchanged/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": null - }, - "hash": 3976463117 + } }, - "hash": 2116468040 + "hash": 4158988729 } } \ No newline at end of file diff --git a/internal/configs/state_store.go b/internal/configs/state_store.go index 82d072fa1d49..cde16f896c6f 100644 --- a/internal/configs/state_store.go +++ b/internal/configs/state_store.go @@ -6,6 +6,7 @@ package configs import ( "fmt" + version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" tfaddr "github.com/hashicorp/terraform-registry-address" @@ -141,7 +142,7 @@ func resolveStateStoreProviderType(requiredProviders map[string]*RequiredProvide // for the purpose of hashing, so that an incomplete configuration can still // be hashed. Other errors, such as extraneous attributes, have no such special // case. -func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema *configschema.Block) (int, tfdiags.Diagnostics) { +func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema *configschema.Block, stateStoreProviderVersion *version.Version) (int, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // 1. Prepare the state_store hash @@ -204,9 +205,10 @@ func (b *StateStore) Hash(stateStoreSchema *configschema.Block, providerSchema * cty.StringVal(b.Type), // state store type ssVal, // state store config - cty.StringVal(b.Provider.Name), // provider name - this reflects the config, whereas provider source is influenced by config but separate - cty.StringVal(b.ProviderAddr.String()), // provider source - pVal, // provider config + cty.StringVal(b.ProviderAddr.String()), // provider source + cty.StringVal(stateStoreProviderVersion.String()), // provider version + cty.StringVal(b.Provider.Name), // provider name - this is directly parsed from the config, whereas provider source is added separately later after config is parsed. + pVal, // provider config }) return toHash.Hash(), diags } diff --git a/internal/configs/state_store_test.go b/internal/configs/state_store_test.go index 91076f718f0c..7fc9eb9f6b3b 100644 --- a/internal/configs/state_store_test.go +++ b/internal/configs/state_store_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfaddr "github.com/hashicorp/terraform-registry-address" @@ -18,28 +19,8 @@ import ( // and it requires calling code to remove the nested provider block from state_store config data. func TestStateStore_Hash(t *testing.T) { - // This test assumes a configuration like this, - // where the "fs" state store is implemented in - // the "foobar" provider: - // - // terraform { - // required_providers = { - // # entries would be here - // } - // state_store "foobar_fs" { - // # Nested provider block - // provider "foobar" { - // foobar = "foobar" - // } - - // # Attributes for configuring the state store - // path = "mystate.tfstate" - // workspace_dir = "foobar" - // } - // } - // Normally these schemas would come from a provider's GetProviderSchema data - stateStoreSchema := &configschema.Block{ + exampleStateStoreSchema := &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "path": { Type: cty.String, @@ -51,7 +32,7 @@ func TestStateStore_Hash(t *testing.T) { }, }, } - providerSchema := &configschema.Block{ + exampleProviderSchema := &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foobar": { Type: cty.String, @@ -60,7 +41,10 @@ func TestStateStore_Hash(t *testing.T) { }, } - // These two values are coupled. + // These values are all coupled. + // The test case below asserts that given these inputs, the expected hash is returned. + exampleProviderVersion := version.Must(version.NewSemver("1.2.3")) + exampleProviderAddr := tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") exampleConfig := configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" @@ -68,22 +52,40 @@ func TestStateStore_Hash(t *testing.T) { path = "mystate.tfstate" workspace_dir = "foobar" }`) - exampleHash := 33464751 + exampleHash := 614398732 + t.Run("example happy path with all attrs set in the configuration", func(t *testing.T) { + // Construct a configs.StateStore for the test. + content, _, cfgDiags := exampleConfig.PartialContent(terraformBlockSchema) + if len(cfgDiags) > 0 { + t.Fatalf("unexpected diagnostics: %s", cfgDiags) + } + var ssDiags hcl.Diagnostics + s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0]) + if len(ssDiags) > 0 { + t.Fatalf("unexpected diagnostics: %s", ssDiags) + } + s.ProviderAddr = exampleProviderAddr + + // Test Hash method. + gotHash, diags := s.Hash(exampleStateStoreSchema, exampleProviderSchema, exampleProviderVersion) + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err()) + } + if gotHash != exampleHash { + t.Fatalf("expected hash for state_store to be %d, but got %d", exampleHash, gotHash) + } + }) + + // Test cases each change a single input that affects the output hash + // Assertions check that the output hash doesn't match the hash above, following the changed input. cases := map[string]struct { config hcl.Body stateStoreSchema *configschema.Block + providerVersion *version.Version providerAddr tfaddr.Provider - wantErrorString string - wantHash int }{ - "example happy path with all attrs set in the configuration": { - stateStoreSchema: stateStoreSchema, - config: exampleConfig, - wantHash: exampleHash, - }, "changing the state store type affects the hash value": { - stateStoreSchema: stateStoreSchema, config: configBodyForTest(t, `state_store "foobar_CHANGED_VALUE_HERE" { provider "foobar" { foobar = "foobar" @@ -91,11 +93,9 @@ func TestStateStore_Hash(t *testing.T) { path = "mystate.tfstate" workspace_dir = "foobar" }`), - wantHash: 559959421, // Differs from `exampleHash` }, "changing the provider affects the hash value": { - stateStoreSchema: stateStoreSchema, - providerAddr: tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "different-provider"), + providerAddr: tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "different-provider"), config: configBodyForTest(t, `state_store "different-provider_fs" { provider "different-provider" { foobar = "foobar" @@ -103,10 +103,11 @@ func TestStateStore_Hash(t *testing.T) { path = "mystate.tfstate" workspace_dir = "foobar" }`), - wantHash: 1672894798, // Differs from `exampleHash` + }, + "changing the provider version affects the hash value": { + providerVersion: version.Must(version.NewSemver("9.9.9")), }, "tolerates empty config block for the provider even when schema has Required field(s)": { - stateStoreSchema: stateStoreSchema, config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { # required field "foobar" is missing @@ -114,10 +115,9 @@ func TestStateStore_Hash(t *testing.T) { path = "mystate.tfstate" workspace_dir = "foobar" }`), - wantHash: 3558227459, }, "tolerates missing Required field(s) in state_store config": { - stateStoreSchema: stateStoreSchema, + stateStoreSchema: exampleStateStoreSchema, config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" @@ -126,10 +126,93 @@ func TestStateStore_Hash(t *testing.T) { # required field "path" is missing workspace_dir = "foobar" }`), - wantHash: 3682853451, }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + // If a test case doesn't set an override for these inputs, + // instead use a default value from the example above. + var config hcl.Body + var schema *configschema.Block + var providerVersion *version.Version + var providerAddr tfaddr.Provider + if tc.config == nil { + config = exampleConfig + } else { + config = tc.config + } + if tc.stateStoreSchema == nil { + schema = exampleStateStoreSchema + } else { + schema = tc.stateStoreSchema + } + if tc.providerVersion == nil { + providerVersion = exampleProviderVersion + } else { + providerVersion = tc.providerVersion + } + if tc.providerAddr.IsZero() { + providerAddr = exampleProviderAddr + } else { + providerAddr = tc.providerAddr + } + + // Construct a configs.StateStore for the test. + content, _, cfgDiags := config.PartialContent(terraformBlockSchema) + if len(cfgDiags) > 0 { + t.Fatalf("unexpected diagnostics: %s", cfgDiags) + } + var ssDiags hcl.Diagnostics + s, ssDiags := decodeStateStoreBlock(content.Blocks.OfType("state_store")[0]) + if len(ssDiags) > 0 { + t.Fatalf("unexpected diagnostics: %s", ssDiags) + } + s.ProviderAddr = providerAddr + + // Test Hash method. + gotHash, diags := s.Hash(schema, exampleProviderSchema, providerVersion) + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err()) + } + if gotHash == exampleHash { + t.Fatal("expected hash for state_store to be different from the example due to a changed input, but it matched.") + } + }) + } +} + +func TestStateStore_Hash_errorConditions(t *testing.T) { + // Normally these schemas would come from a provider's GetProviderSchema data + exampleStateStoreSchema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "path": { + Type: cty.String, + Required: true, + }, + "workspace_dir": { + Type: cty.String, + Optional: true, + }, + }, + } + exampleProviderSchema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foobar": { + Type: cty.String, + Required: true, + }, + }, + } + + // Cases where an error would occur + cases := map[string]struct { + config hcl.Body + stateStoreSchema *configschema.Block + wantErrorString string + }{ "returns errors when the state_store config doesn't match the schema": { - stateStoreSchema: stateStoreSchema, + stateStoreSchema: exampleStateStoreSchema, config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" @@ -144,7 +227,7 @@ func TestStateStore_Hash(t *testing.T) { wantErrorString: "Unsupported argument", }, "returns errors when the provider config doesn't match the schema": { - stateStoreSchema: stateStoreSchema, + stateStoreSchema: exampleStateStoreSchema, config: configBodyForTest(t, `state_store "foobar_fs" { provider "foobar" { foobar = "foobar" @@ -215,32 +298,15 @@ func TestStateStore_Hash(t *testing.T) { if len(ssDiags) > 0 { t.Fatalf("unexpected diagnostics: %s", ssDiags) } - // Add provider addr - if tc.providerAddr.IsZero() { - s.ProviderAddr = tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") - } else { - s.ProviderAddr = tc.providerAddr - } + s.ProviderAddr = tfaddr.NewProvider(tfaddr.DefaultProviderRegistryHost, "hashicorp", "foobar") // Test Hash method. - gotHash, diags := s.Hash(tc.stateStoreSchema, providerSchema) - if diags.HasErrors() { - if tc.wantErrorString == "" { - t.Fatalf("unexpected error: %s", diags.Err()) - } - if !strings.Contains(diags.Err().Error(), tc.wantErrorString) { - t.Fatalf("expected %q to be in the returned error string but it's missing: %q", tc.wantErrorString, diags.Err()) - } - - return // early return if testing an error case - } - - if !diags.HasErrors() && tc.wantErrorString != "" { - t.Fatal("expected an error when generating a hash, but got none") + _, diags := s.Hash(tc.stateStoreSchema, exampleProviderSchema, version.Must(version.NewSemver("1.2.3"))) + if !diags.HasErrors() { + t.Fatal("expected error but got none") } - - if gotHash != tc.wantHash { - t.Fatalf("expected hash for state_store to be %d, but got %d", tc.wantHash, gotHash) + if !strings.Contains(diags.Err().Error(), tc.wantErrorString) { + t.Fatalf("expected error to contain %q but got: %s", tc.wantErrorString, diags.Err()) } }) } From ce766f5a032a0611e5132524a2d699fee76bfdec Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 17:52:57 +0100 Subject: [PATCH 34/38] Update tests that now require locks data to be present in test setup --- internal/command/meta_backend_test.go | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 7ce96dd5d6b9..116940796a72 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -2151,10 +2151,23 @@ func TestMetaBackend_configuredBackendToStateStore(t *testing.T) { mock := testStateStoreMock(t) // Get the operations backend + locks := depsfile.NewLocks() + providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") + constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") + if err != nil { + t.Fatalf("test setup failed when making constraint: %s", err) + } + locks.SetProvider( + providerAddr, + versions.MustParseVersion("9.9.9"), + constraint, + []providerreqs.Hash{""}, + ) _, beDiags := m.Backend(&BackendOpts{ Init: true, StateStoreConfig: mod.StateStore, ProviderFactory: providers.FactoryFixed(mock), + Locks: locks, }) if !beDiags.HasErrors() { t.Fatal("expected an error to be returned during partial implementation of PSS") @@ -2207,6 +2220,19 @@ func TestMetaBackend_configuredStateStoreToBackend(t *testing.T) { func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { wantErr := "Variables not allowed" + locks := depsfile.NewLocks() + providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") + constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") + if err != nil { + t.Fatalf("test setup failed when making constraint: %s", err) + } + locks.SetProvider( + providerAddr, + versions.MustParseVersion("9.9.9"), + constraint, + []providerreqs.Hash{""}, + ) + cases := map[string]struct { fixture string wantErr string @@ -2249,6 +2275,7 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) { Init: true, StateStoreConfig: mod.StateStore, ProviderFactory: providers.FactoryFixed(mock), + Locks: locks, }) if err == nil { t.Fatal("should error") @@ -2684,6 +2711,19 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { ProviderAddr: addrs.NewDefaultProvider("test"), } + locks := depsfile.NewLocks() + providerAddr := addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test") + constraint, err := providerreqs.ParseVersionConstraints(">1.0.0") + if err != nil { + t.Fatalf("test setup failed when making constraint: %s", err) + } + locks.SetProvider( + providerAddr, + versions.MustParseVersion("9.9.9"), + constraint, + []providerreqs.Hash{""}, + ) + t.Run("override config can change values of custom attributes in the state_store block", func(t *testing.T) { overrideValue := "overridden" configOverride := configs.SynthBody("synth", map[string]cty.Value{"value": cty.StringVal(overrideValue)}) @@ -2693,6 +2733,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { ConfigOverride: configOverride, ProviderFactory: providers.FactoryFixed(mock), Init: true, + Locks: locks, } m := testMetaBackend(t, nil) @@ -2720,6 +2761,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { opts := &BackendOpts{ StateStoreConfig: nil, //unset Init: true, + Locks: locks, } m := testMetaBackend(t, nil) @@ -2741,6 +2783,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { StateStoreConfig: config, ProviderFactory: nil, // unset Init: true, + Locks: locks, } m := testMetaBackend(t, nil) @@ -2765,6 +2808,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { StateStoreConfig: config, ProviderFactory: providers.FactoryFixed(mock), Init: true, + Locks: locks, } m := testMetaBackend(t, nil) @@ -2792,6 +2836,7 @@ func TestMetaBackend_stateStoreConfig(t *testing.T) { StateStoreConfig: config, ProviderFactory: providers.FactoryFixed(mock), Init: true, + Locks: locks, } m := testMetaBackend(t, nil) From e585e614bfb56ddbdaf6ca7a4c423892331ea233 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 17:55:15 +0100 Subject: [PATCH 35/38] Update comment for accuracy --- internal/configs/state_store.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/configs/state_store.go b/internal/configs/state_store.go index cde16f896c6f..8d5fb850fb73 100644 --- a/internal/configs/state_store.go +++ b/internal/configs/state_store.go @@ -131,9 +131,12 @@ func resolveStateStoreProviderType(requiredProviders map[string]*RequiredProvide } } -// Hash produces a hash value for the receiver that covers the type and the -// portions of the config that conform to the state_store schema. The provider -// block that is nested inside state_store is ignored. +// Hash produces a hash value for the receiver that covers: +// 1) the portions of the config that conform to the state_store schema. +// 2) the portions of the config that conform to the provider schema. +// 3) the state store type +// 4) the provider source +// 5) the provider version // // If the config does not conform to the schema then the result is not // meaningful for comparison since it will be based on an incomplete result. From 848b3694eaeeee455d363c02ba5cf891764552e7 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Fri, 17 Oct 2025 17:58:32 +0100 Subject: [PATCH 36/38] Fixes to other test fixtures - remove excess hash field, set hash to 0 to indicate they're not set accurately. --- .../state-store-to-backend/.terraform/terraform.tfstate | 5 ++--- .../testdata/state-store-unset/.terraform/terraform.tfstate | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate b/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate index 1a09f5af080d..b7e79f249766 100644 --- a/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-to-backend/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": null - }, - "hash": 3976463117 + } }, - "hash": 2116468040 + "hash": 0 } } \ No newline at end of file diff --git a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate index 1a09f5af080d..b7e79f249766 100644 --- a/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate +++ b/internal/command/testdata/state-store-unset/.terraform/terraform.tfstate @@ -12,9 +12,8 @@ "source": "registry.terraform.io/hashicorp/test", "config": { "region": null - }, - "hash": 3976463117 + } }, - "hash": 2116468040 + "hash": 0 } } \ No newline at end of file From 82f939a60fabbc2201da997d4f37f7e55ad9ba72 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 13:18:19 +0100 Subject: [PATCH 37/38] Make upgrade test actually use upgrade code path --- internal/command/init_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 91a6ffe51d27..f14c35b08b75 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -4001,6 +4001,7 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) { args := []string{ "-enable-pluggable-state-storage-experiment=true", + "-upgrade", } code := c.Run(args) testOutput := done(t) From 24be93c8e480d41df34860c2262c4a4214d89ec1 Mon Sep 17 00:00:00 2001 From: Sarah French Date: Mon, 20 Oct 2025 13:18:24 +0100 Subject: [PATCH 38/38] Add lock files to test fixture directories that represent a project that's had a successful prior init using PSS --- .../state-store-changed/provider-config/.terraform.lock.hcl | 6 ++++++ .../provider-upgraded/.terraform.lock.hcl | 6 ++++++ .../state-store-changed/provider-used/.terraform.lock.hcl | 6 ++++++ .../testdata/state-store-to-backend/.terraform.lock.hcl | 6 ++++++ .../testdata/state-store-unchanged/.terraform.lock.hcl | 6 ++++++ .../command/testdata/state-store-unset/.terraform.lock.hcl | 6 ++++++ 6 files changed, 36 insertions(+) create mode 100644 internal/command/testdata/state-store-changed/provider-config/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-store-changed/provider-upgraded/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-store-changed/provider-used/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-store-to-backend/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-store-unchanged/.terraform.lock.hcl create mode 100644 internal/command/testdata/state-store-unset/.terraform.lock.hcl diff --git a/internal/command/testdata/state-store-changed/provider-config/.terraform.lock.hcl b/internal/command/testdata/state-store-changed/provider-config/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-config/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-store-changed/provider-upgraded/.terraform.lock.hcl b/internal/command/testdata/state-store-changed/provider-upgraded/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-upgraded/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-store-changed/provider-used/.terraform.lock.hcl b/internal/command/testdata/state-store-changed/provider-used/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-store-changed/provider-used/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-store-to-backend/.terraform.lock.hcl b/internal/command/testdata/state-store-to-backend/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-store-to-backend/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-store-unchanged/.terraform.lock.hcl b/internal/command/testdata/state-store-unchanged/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-store-unchanged/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" +} diff --git a/internal/command/testdata/state-store-unset/.terraform.lock.hcl b/internal/command/testdata/state-store-unset/.terraform.lock.hcl new file mode 100644 index 000000000000..e5c03757a7fa --- /dev/null +++ b/internal/command/testdata/state-store-unset/.terraform.lock.hcl @@ -0,0 +1,6 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.2.3" +}