mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 00:42:48 -04:00
PSS: Enable state store configuration change (#38153)
This commit is contained in:
parent
c37cf1c0f5
commit
cd9257cd53
6 changed files with 696 additions and 71 deletions
|
|
@ -4155,7 +4155,7 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("handling changed state store config is currently unimplemented", func(t *testing.T) {
|
||||
t.Run("handling changed state store config without -migrate-state flag", func(t *testing.T) {
|
||||
// Create a temporary working directory with state store configuration
|
||||
// that doesn't match the backend state file
|
||||
td := t.TempDir()
|
||||
|
|
@ -4189,30 +4189,34 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
// missing -migrate-state flag
|
||||
}
|
||||
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())
|
||||
if code == 0 {
|
||||
t.Fatalf("expected non-zero 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"
|
||||
expectedMsg := "Error: State store configuration changed"
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handling changed state store provider config is currently unimplemented", func(t *testing.T) {
|
||||
t.Run("handling changed state store config", 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)
|
||||
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.
|
||||
// The previous init implied by this test scenario would have created the default workspace.
|
||||
mockProvider.MockStates = map[string]any{
|
||||
backend.DefaultStateName: []byte(`{"version":4,"terraform_version":"1.15.0","serial":1,"lineage":"91adaece-23b3-7bce-0695-5aea537d2fef","outputs":{"test":{"value":"test","type":"string"}},"resources":[],"check_results":null}`),
|
||||
}
|
||||
mockProviderAddress := addrs.NewDefaultProvider("test")
|
||||
providerSource, close := newMockProviderSource(t, map[string][]string{
|
||||
"hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture
|
||||
|
|
@ -4238,22 +4242,94 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
"-force-copy",
|
||||
}
|
||||
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())
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 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"
|
||||
expectedMsg := "Terraform has been successfully initialized!"
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output)
|
||||
}
|
||||
expectedReason := "State store \"test_store\" (hashicorp/test) configuration changed"
|
||||
if !strings.Contains(output, expectedReason) {
|
||||
t.Fatalf("expected output to include reason %q, but got':\n %s", expectedReason, output)
|
||||
}
|
||||
|
||||
// check state remains accessible after migration
|
||||
if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists {
|
||||
t.Fatalf("expected the default workspace to exist after migration, but it is missing: %#v", mockProvider.MockStates)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handling changed state store type in the same provider is currently unimplemented", func(t *testing.T) {
|
||||
t.Run("handling changed state store provider config", 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()
|
||||
// The previous init implied by this test scenario would have created the default workspace.
|
||||
mockProvider.MockStates = map[string]any{
|
||||
backend.DefaultStateName: []byte(`{"version":4,"terraform_version":"1.15.0","serial":1,"lineage":"91adaece-23b3-7bce-0695-5aea537d2fef","outputs":{"test":{"value":"test","type":"string"}},"resources":[],"check_results":null}`),
|
||||
}
|
||||
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",
|
||||
"-force-copy",
|
||||
}
|
||||
code := c.Run(args)
|
||||
testOutput := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 exit code, got %d, output: \n%s", code, testOutput.All())
|
||||
}
|
||||
|
||||
// Check output
|
||||
output := testOutput.All()
|
||||
expectedMsg := "Terraform has been successfully initialized!"
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output)
|
||||
}
|
||||
expectedReason := "State store provider \"test\" (hashicorp/test) configuration changed"
|
||||
if !strings.Contains(output, expectedReason) {
|
||||
t.Fatalf("expected output to include reason %q, but got':\n %s", expectedReason, output)
|
||||
}
|
||||
|
||||
// check state remains accessible after migration
|
||||
if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists {
|
||||
t.Fatal("expected the default workspace to exist after migration, but it is missing")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handling changed state store type in the same provider", func(t *testing.T) {
|
||||
// Create a temporary working directory with state store configuration
|
||||
// that doesn't match the backend state file
|
||||
td := t.TempDir()
|
||||
|
|
@ -4261,6 +4337,10 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
t.Chdir(td)
|
||||
|
||||
mockProvider := mockPluggableStateStorageProvider()
|
||||
// The previous init implied by this test scenario would have created the default workspace.
|
||||
mockProvider.MockStates = map[string]any{
|
||||
backend.DefaultStateName: []byte(`{"version":4,"terraform_version":"1.15.0","serial":1,"lineage":"91adaece-23b3-7bce-0695-5aea537d2fef","outputs":{"test":{"value":"test","type":"string"}},"resources":[],"check_results":null}`),
|
||||
}
|
||||
storeName := "test_store"
|
||||
otherStoreName := "test_otherstore"
|
||||
// Make the provider report that it contains a 2nd storage implementation with the above name
|
||||
|
|
@ -4290,22 +4370,32 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
"-force-copy",
|
||||
}
|
||||
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())
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 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"
|
||||
expectedMsg := "Terraform has been successfully initialized!"
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output)
|
||||
}
|
||||
expectedReason := "State store type changed from \"test_store\" to \"test_otherstore\""
|
||||
if !strings.Contains(output, expectedReason) {
|
||||
t.Fatalf("expected output to include reason %q, but got':\n %s", expectedReason, output)
|
||||
}
|
||||
|
||||
// check state remains accessible after migration
|
||||
if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists {
|
||||
t.Fatal("expected the default workspace to exist after migration, but it is missing")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handling changing the provider used for state storage is currently unimplemented", func(t *testing.T) {
|
||||
t.Run("handling changing the provider used for state storage", func(t *testing.T) {
|
||||
// Create a temporary working directory with state store configuration
|
||||
// that doesn't match the backend state file
|
||||
td := t.TempDir()
|
||||
|
|
@ -4313,7 +4403,10 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
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.
|
||||
// The previous init implied by this test scenario would have created the default workspace.
|
||||
mockProvider.MockStates = map[string]any{
|
||||
backend.DefaultStateName: []byte(`{"version":4,"terraform_version":"1.15.0","serial":1,"lineage":"91adaece-23b3-7bce-0695-5aea537d2fef","outputs":{"test":{"value":"test","type":"string"}},"resources":[],"check_results":null}`),
|
||||
}
|
||||
|
||||
// Make a mock that implies its name is test2 based on returned schemas
|
||||
mockProvider2 := mockPluggableStateStorageProvider()
|
||||
|
|
@ -4348,19 +4441,29 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
"-force-copy",
|
||||
}
|
||||
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())
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 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"
|
||||
expectedMsg := "Terraform has been successfully initialized!"
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output)
|
||||
}
|
||||
expectedReason := "State store provider changed from hashicorp/test to hashicorp/test2"
|
||||
if !strings.Contains(output, expectedReason) {
|
||||
t.Fatalf("expected output to include reason %q, but got':\n %s", expectedReason, output)
|
||||
}
|
||||
|
||||
// check state remains accessible after migration
|
||||
if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists {
|
||||
t.Fatal("expected the default workspace to exist after migration, but it is missing")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -4370,7 +4473,7 @@ func TestInit_stateStore_configChanges(t *testing.T) {
|
|||
// 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) {
|
||||
t.Run("handling upgrading the provider used for state storage", func(t *testing.T) {
|
||||
// Create a temporary working directory with state store configuration
|
||||
// that doesn't match the backend state file
|
||||
td := t.TempDir()
|
||||
|
|
@ -4378,6 +4481,10 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) {
|
|||
t.Chdir(td)
|
||||
|
||||
mockProvider := mockPluggableStateStorageProvider()
|
||||
// The previous init implied by this test scenario would have created the default workspace.
|
||||
mockProvider.MockStates = map[string]any{
|
||||
backend.DefaultStateName: []byte(`{"version":4,"terraform_version":"1.15.0","serial":1,"lineage":"91adaece-23b3-7bce-0695-5aea537d2fef","outputs":{"test":{"value":"test","type":"string"}},"resources":[],"check_results":null}`),
|
||||
}
|
||||
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
|
||||
|
|
@ -4403,23 +4510,147 @@ func TestInit_stateStore_providerUpgrade(t *testing.T) {
|
|||
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
"-migrate-state=true",
|
||||
"-upgrade",
|
||||
}
|
||||
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())
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 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"
|
||||
expectedMsg := "Terraform has been successfully initialized!"
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Fatalf("expected output to include %q, but got':\n %s", expectedMsg, output)
|
||||
}
|
||||
expectedReason := "State store provider \"test\" (hashicorp/test) version changed from 1.2.3 to 9.9.9"
|
||||
if !strings.Contains(output, expectedReason) {
|
||||
t.Fatalf("expected output to include reason %q, but got':\n %s", expectedReason, output)
|
||||
}
|
||||
|
||||
// check state remains accessible after migration
|
||||
if _, exists := mockProvider.MockStates[backend.DefaultStateName]; !exists {
|
||||
t.Fatal("expected the default workspace to exist after migration, but it is missing")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test a scenario where the configuration changes but the -backend-config CLI flags compensate for those changes
|
||||
// to result in the state store being configured in the same way. In this scenario the user isn't prompted to migrate
|
||||
// state but the backend state file is updated with a new hash to reflect the new configuration's values.
|
||||
func TestInit_stateStore_backendConfigFlagNoMigrate(t *testing.T) {
|
||||
// Create a temporary working directory and copy in test fixtures
|
||||
td := t.TempDir()
|
||||
testCopyDir(t, testFixturePath("init-state-store"), td)
|
||||
t.Chdir(td)
|
||||
|
||||
mockProvider := mockPluggableStateStorageProvider()
|
||||
// Make the state store's value attribute optional in this test.
|
||||
mockProvider.GetProviderSchemaResponse.StateStores["test_store"].Body.Attributes["value"].Required = false
|
||||
|
||||
mockProviderAddress := addrs.NewDefaultProvider("test")
|
||||
providerSource, close := newMockProviderSource(t, map[string][]string{
|
||||
"hashicorp/test": {"1.2.3"}, // Matches provider version in backend state file fixture
|
||||
})
|
||||
t.Cleanup(close)
|
||||
|
||||
var originalStateStoreConfigHash uint64
|
||||
|
||||
{
|
||||
log.Printf("[TRACE] TestInit_stateStore_unset: beginning first init")
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &InitCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: &testingOverrides{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
mockProviderAddress: providers.FactoryFixed(mockProvider),
|
||||
},
|
||||
},
|
||||
ProviderSource: providerSource,
|
||||
Ui: ui,
|
||||
View: view,
|
||||
AllowExperimentalFeatures: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Init
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
}
|
||||
code := c.Run(args)
|
||||
testOutput := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: \n%s", testOutput.All())
|
||||
}
|
||||
log.Printf("[TRACE] TestInit_stateStore_unset: first init complete")
|
||||
t.Logf("First run output:\n%s", testOutput.Stdout())
|
||||
t.Logf("First run errors:\n%s", testOutput.Stderr())
|
||||
|
||||
s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
|
||||
if s.StateStore.Empty() {
|
||||
t.Fatal("should have StateStore config")
|
||||
}
|
||||
// Store this for comparison after the 2nd init
|
||||
originalStateStoreConfigHash = s.StateStore.Hash
|
||||
}
|
||||
|
||||
{
|
||||
log.Printf("[TRACE] TestInit_stateStore_unset: beginning second init with changed config but compensating CLI flags")
|
||||
|
||||
// Remove `value` attribute from config
|
||||
cfg := `terraform {
|
||||
state_store "test_store" {
|
||||
provider "test" {}
|
||||
# value attr removed here
|
||||
}
|
||||
}`
|
||||
if err := os.WriteFile("main.tf", []byte(cfg), 0644); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
view, done := testView(t)
|
||||
c := &InitCommand{
|
||||
Meta: Meta{
|
||||
testingOverrides: &testingOverrides{
|
||||
Providers: map[addrs.Provider]providers.Factory{
|
||||
mockProviderAddress: providers.FactoryFixed(mockProvider),
|
||||
},
|
||||
},
|
||||
ProviderSource: providerSource,
|
||||
Ui: ui,
|
||||
View: view,
|
||||
AllowExperimentalFeatures: true,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-enable-pluggable-state-storage-experiment=true",
|
||||
"-backend-config=value=foobar", // value = foobar, matches the line removed from config
|
||||
}
|
||||
code := c.Run(args)
|
||||
testOutput := done(t)
|
||||
if code != 0 {
|
||||
t.Fatalf("Terraform either experienced an unexpected error, or suggested a state migration when this test scenario should not include migrations: \n%s", testOutput.All())
|
||||
}
|
||||
log.Printf("[TRACE] TestInit_stateStore_unset: second init complete")
|
||||
t.Logf("Second run output:\n%s", testOutput.Stdout())
|
||||
t.Logf("Second run errors:\n%s", testOutput.Stderr())
|
||||
|
||||
s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename))
|
||||
if s.StateStore.Empty() {
|
||||
t.Fatal("should have StateStore config")
|
||||
}
|
||||
if s.StateStore.Hash == originalStateStoreConfigHash {
|
||||
t.Fatal("expected second init to update the state_store config hash in the backend state file, but it did not")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit_stateStore_unset(t *testing.T) {
|
||||
// Create a temporary working directory and copy in test fixtures
|
||||
td := t.TempDir()
|
||||
|
|
@ -6085,6 +6316,7 @@ func expectedPackageInstallPath(name, version string, exe bool) string {
|
|||
))
|
||||
}
|
||||
|
||||
// TODO: introduce pssName as argument here to aid testing migrations
|
||||
func mockPluggableStateStorageProvider() *testing_provider.MockProvider {
|
||||
// Create a mock provider to use for PSS
|
||||
// Get mock provider factory to be used during init
|
||||
|
|
@ -6127,6 +6359,12 @@ func mockPluggableStateStorageProvider() *testing_provider.MockProvider {
|
|||
},
|
||||
},
|
||||
}
|
||||
mock.GetStatesFn = func(req providers.GetStatesRequest) providers.GetStatesResponse {
|
||||
states := slices.Sorted(maps.Keys(mock.MockStates))
|
||||
return providers.GetStatesResponse{
|
||||
States: states,
|
||||
}
|
||||
}
|
||||
mock.ConfigureStateStoreFn = func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
|
||||
return providers.ConfigureStateStoreResponse{
|
||||
Capabilities: providers.StateStoreServerCapabilities{
|
||||
|
|
|
|||
|
|
@ -1027,9 +1027,10 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
|
|||
s.StateStore.Provider.Source,
|
||||
)
|
||||
|
||||
initReason := fmt.Sprintf("Unsetting the previously set state store %q", s.StateStore.Type)
|
||||
if !opts.Init {
|
||||
diags = diags.Append(errStateStoreInitDiag(initReason))
|
||||
diags = diags.Append(errStateStoreInitDiag(&ssInitReason{
|
||||
Reason: fmt.Sprintf("Unsetting the previously set state store %q", s.StateStore.Type),
|
||||
}))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
|
|
@ -1076,12 +1077,15 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
|
|||
)
|
||||
|
||||
if !opts.Init {
|
||||
initReason := fmt.Sprintf("Initial configuration of the requested state_store %q in provider %s (%q)",
|
||||
reason := fmt.Sprintf("Initial configuration of a state store %q in provider %s (%q)",
|
||||
stateStoreConfig.Type,
|
||||
stateStoreConfig.Provider.Name,
|
||||
stateStoreConfig.ProviderAddr,
|
||||
)
|
||||
diags = diags.Append(errStateStoreInitDiag(initReason))
|
||||
diags = diags.Append(errStateStoreInitDiag(&ssInitReason{
|
||||
Reason: reason,
|
||||
Subject: stateStoreConfig.DeclRange.Ptr(),
|
||||
}))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
|
|
@ -1213,15 +1217,8 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
|
|||
// Potentially changing a state store configuration
|
||||
case backendConfig == nil && s.Backend.Empty() &&
|
||||
stateStoreConfig != nil && !s.StateStore.Empty():
|
||||
// When implemented, this will need to handle multiple scenarios like:
|
||||
// > Changing to using a different provider for PSS.
|
||||
// > Changing to using a different version of the same provider for PSS.
|
||||
// >>>> Navigating state upgrades that do not force an explicit migration &&
|
||||
// identifying when migration is required.
|
||||
// > Changing to using a different store in the same version of the provider.
|
||||
// > Changing how the provider is configured.
|
||||
// > Changing how the store is configured.
|
||||
// > Allowing values to be moved between partial overrides and config
|
||||
// TODO: Navigating state upgrades that do not force an explicit migration &&
|
||||
// identifying when migration is required.
|
||||
|
||||
// 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.
|
||||
|
|
@ -1240,14 +1237,56 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di
|
|||
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",
|
||||
Detail: "Changing a state store configuration is not implemented yet",
|
||||
})
|
||||
// If our configuration (the result of both the literal configuration and given
|
||||
// -backend-config options) is the same, then we're just initializing a previously
|
||||
// configured state store. The literal configuration may differ, however, so while we
|
||||
// don't need to migrate, we update the state store cache hash value.
|
||||
if !m.stateStoreConfigNeedsMigration(stateStoreConfig, s.StateStore, opts) {
|
||||
log.Printf("[TRACE] Meta.Backend: using already-initialized %q state store configuration", stateStoreConfig.Type)
|
||||
savedStateStore, moreDiags := m.savedStateStore(sMgr)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// It's possible for a state store 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.updateSavedStateStoreHash(cHash, sMgr)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
initReason, ssDiags := m.determineStateStoreInitReason(s.StateStore, stateStoreConfig, opts.Locks)
|
||||
diags = diags.Append(ssDiags)
|
||||
if ssDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if !opts.Init {
|
||||
// user ran another cmd that is not init but they are required to initialize
|
||||
diags = diags.Append(errStateStoreInitDiag(initReason))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
log.Printf("[WARN] state store has changed since last init: %q", initReason.Reason)
|
||||
|
||||
if !m.migrateState {
|
||||
diags = diags.Append(migrateOrReconfigStateStoreDiag)
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return m.stateStore_changed(stateStoreConfig, cHash, sMgr, opts, initReason)
|
||||
|
||||
default:
|
||||
diags = diags.Append(fmt.Errorf(
|
||||
|
|
@ -1302,6 +1341,109 @@ func (m *Meta) determineInitReason(previousBackendType string, currentBackendTyp
|
|||
return diags
|
||||
}
|
||||
|
||||
// determineStateStoreInitReason determines the reason for the state store configuration change.
|
||||
//
|
||||
// When a reason cannot be determined and the underlying problem
|
||||
// is likely to be addressed by the migration, a warning diagnostic is returned,
|
||||
// otherwise an error diagnostic is returned.
|
||||
func (m *Meta) determineStateStoreInitReason(cfgState *workdir.StateStoreConfigState, cfg *configs.StateStore, pLocks *depsfile.Locks) (*ssInitReason, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if cfgState == nil || cfgState.Empty() {
|
||||
return &ssInitReason{
|
||||
Reason: fmt.Sprintf("State store %q (%s) not initialised",
|
||||
cfg.Type, cfg.ProviderAddr.ForDisplay()),
|
||||
Subject: cfg.DeclRange.Ptr(),
|
||||
}, diags
|
||||
}
|
||||
if !cfg.ProviderAddr.Equals(*cfgState.Provider.Source) {
|
||||
return &ssInitReason{
|
||||
Reason: fmt.Sprintf("State store provider changed from %s to %s",
|
||||
cfgState.Provider.Source.ForDisplay(), cfg.ProviderAddr.ForDisplay()),
|
||||
Subject: cfg.Provider.DeclRange.Ptr(),
|
||||
}, diags
|
||||
}
|
||||
|
||||
pLock := pLocks.Provider(cfg.ProviderAddr)
|
||||
lockVersion, err := providerreqs.GoVersionFromVersion(pLock.Version())
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Unable to determine version of the state store provider",
|
||||
fmt.Sprintf("Failed to parse version of %s from the lock file: %s", cfg.ProviderAddr.ForDisplay(), err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
if !lockVersion.Equal(cfgState.Provider.Version) {
|
||||
return &ssInitReason{
|
||||
Reason: fmt.Sprintf("State store provider %q (%s) version changed from %s to %s",
|
||||
cfg.Provider.Name, cfg.ProviderAddr.ForDisplay(),
|
||||
cfgState.Provider.Version, lockVersion),
|
||||
}, diags
|
||||
}
|
||||
|
||||
if cfgState.Type != cfg.Type {
|
||||
return &ssInitReason{
|
||||
Reason: fmt.Sprintf("State store type changed from %q to %q",
|
||||
cfgState.Type, cfg.Type),
|
||||
Subject: cfg.TypeRange.Ptr(),
|
||||
}, diags
|
||||
}
|
||||
|
||||
// We need the state store schema to do our comparisons here.
|
||||
ssBackend, ssCfgVal, pCfgVal, ssDiags := m.stateStoreInitFromConfig(cfg, pLocks)
|
||||
if ssDiags.HasErrors() {
|
||||
return nil, ssDiags
|
||||
}
|
||||
|
||||
// change of provider configuration
|
||||
cachedProviderVal, err := cfgState.Provider.Config(ssBackend.ProviderSchema())
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Unable to decode state_store provider configuration",
|
||||
fmt.Sprintf("Failed to decode configuration of the provider %q (%s): %s",
|
||||
cfg.Provider.Name, cfg.ProviderAddr.ForDisplay(), err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
if !cachedProviderVal.RawEquals(pCfgVal) {
|
||||
return &ssInitReason{
|
||||
Reason: fmt.Sprintf("State store provider %q (%s) configuration changed",
|
||||
cfg.Provider.Name, cfg.ProviderAddr.ForDisplay()),
|
||||
Subject: cfg.Provider.DeclRange.Ptr(),
|
||||
}, diags
|
||||
}
|
||||
|
||||
// change of state_store configuration
|
||||
cachedSsVal, err := cfgState.Config(ssBackend.ConfigSchema())
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Warning,
|
||||
"Unable to decode state_store configuration",
|
||||
fmt.Sprintf("Failed to decode configuration of state_store %q: %s",
|
||||
cfg.Type, err),
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if !cachedSsVal.RawEquals(ssCfgVal) {
|
||||
return &ssInitReason{
|
||||
Reason: fmt.Sprintf("State store %q (%s) configuration changed",
|
||||
cfg.Type, cfg.ProviderAddr.ForDisplay()),
|
||||
Subject: cfg.DeclRange.Ptr(),
|
||||
}, diags
|
||||
}
|
||||
|
||||
// The above conditions should have covered all possible reasons
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Unable to determine state store init reason",
|
||||
"This is a bug in Terraform and should be reported",
|
||||
))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// backendFromState returns the initialized (not configured) backend directly
|
||||
// from the backend state. This should be used only when a user runs
|
||||
// `terraform init -backend=false`. This function returns a local backend if
|
||||
|
|
@ -1863,6 +2005,22 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
|
|||
return diags
|
||||
}
|
||||
|
||||
func (m *Meta) updateSavedStateStoreHash(cHash int, sMgr *clistate.LocalState) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
s := sMgr.State()
|
||||
|
||||
if s.StateStore.Hash != uint64(cHash) {
|
||||
s.StateStore.Hash = uint64(cHash)
|
||||
if err := sMgr.WriteState(s); err != nil {
|
||||
diags = diags.Append(errStateStoreWriteSavedDiag(err))
|
||||
}
|
||||
// No need to call PersistState as it's a no-op
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// backend returns an operations backend that may use a backend, cloud, or state_store block for state storage.
|
||||
// Based on the supplied config, it prepares arguments to pass into (Meta).Backend, which returns the operations backend.
|
||||
//
|
||||
|
|
@ -2061,11 +2219,7 @@ func (m *Meta) backend_to_stateStore(bcs *workdir.BackendConfigState, sMgr *clis
|
|||
return nil, diags
|
||||
}
|
||||
|
||||
// We need to briefly convert away from backend.Backend interface to use the method
|
||||
// for accessing the provider schema. In this method we _always_ expect the concrete value
|
||||
// to be backendPluggable.Pluggable.
|
||||
plug := ssBackend.(*backendPluggable.Pluggable)
|
||||
err = s.StateStore.Provider.SetConfig(providerConfigVal, plug.ProviderSchema())
|
||||
err = s.StateStore.Provider.SetConfig(providerConfigVal, ssBackend.ProviderSchema())
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to set state store provider configuration: %w", err))
|
||||
return nil, diags
|
||||
|
|
@ -2295,11 +2449,7 @@ func (m *Meta) stateStore_C_s(c *configs.StateStore, stateStoreHash int, backend
|
|||
return nil, diags
|
||||
}
|
||||
|
||||
// We need to briefly convert away from backend.Backend interface to use the method
|
||||
// for accessing the provider schema. In this method we _always_ expect the concrete value
|
||||
// to be backendPluggable.Pluggable.
|
||||
plug := b.(*backendPluggable.Pluggable)
|
||||
err = s.StateStore.Provider.SetConfig(providerConfigVal, plug.ProviderSchema())
|
||||
err = s.StateStore.Provider.SetConfig(providerConfigVal, b.ProviderSchema())
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to set state store provider configuration: %w", err))
|
||||
return nil, diags
|
||||
|
|
@ -2418,6 +2568,205 @@ func (m *Meta) stateStore_to_backend(ssSMgr *clistate.LocalState, dstBackendType
|
|||
return dstBackend, diags
|
||||
}
|
||||
|
||||
// stateStoreConfigNeedsMigration returns true if migration might be required to
|
||||
// move from the configured state store to the given cached state store config.
|
||||
//
|
||||
// This must be called with the synthetic *configs.StateStore that results from
|
||||
// merging in any command-line options for correct behavior.
|
||||
//
|
||||
// If either the given configuration or cached configuration are invalid then
|
||||
// this function will conservatively assume that migration is required,
|
||||
// expecting that the migration code will subsequently deal with the same
|
||||
// errors.
|
||||
func (m *Meta) stateStoreConfigNeedsMigration(cfg *configs.StateStore, cfgState *workdir.StateStoreConfigState, opts *BackendOpts) bool {
|
||||
if cfgState == nil || cfgState.Empty() {
|
||||
log.Print("[TRACE] stateStoreConfigNeedsMigration: no cached config, so migration is required")
|
||||
return true
|
||||
}
|
||||
|
||||
if !cfg.ProviderAddr.Equals(*cfgState.Provider.Source) {
|
||||
log.Printf("[TRACE] stateStoreConfigNeedsMigration: provider changed from %q to %q, so migration is required", cfgState.Provider.Source, cfg.ProviderAddr)
|
||||
return true
|
||||
}
|
||||
|
||||
pLock := opts.Locks.Provider(cfg.ProviderAddr)
|
||||
pVersion, err := providerreqs.GoVersionFromVersion(pLock.Version())
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] stateStoreConfigNeedsMigration: unable to determine provider version (%s), so migration is required", err)
|
||||
return true // let the migration codepath deal with the error
|
||||
}
|
||||
if !pVersion.Equal(cfgState.Provider.Version) {
|
||||
log.Printf("[TRACE] stateStoreConfigNeedsMigration: provider version changed from %q to %q, so migration is required", cfgState.Provider.Version, pVersion)
|
||||
return true
|
||||
}
|
||||
|
||||
if cfg.Type != cfgState.Type {
|
||||
log.Printf("[TRACE] stateStoreConfigNeedsMigration: type changed from %q to %q, so migration is required", cfgState.Type, cfg.Type)
|
||||
return true
|
||||
}
|
||||
|
||||
// We need the state store schema to do our comparisons here.
|
||||
ssBackend, ssCfgVal, pCfgVal, ssDiags := m.stateStoreInitFromConfig(cfg, opts.Locks)
|
||||
if ssDiags.HasErrors() {
|
||||
log.Printf("[ERROR] Unable to initialise state store: %s", ssDiags)
|
||||
return true
|
||||
}
|
||||
|
||||
// change of provider configuration
|
||||
cachedProviderVal, err := cfgState.Provider.Config(ssBackend.ProviderSchema())
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Unable to decode cached provider configuration: %s", err)
|
||||
return true
|
||||
}
|
||||
if !cachedProviderVal.RawEquals(pCfgVal) {
|
||||
log.Printf("[TRACE] stateStoreConfigNeedsMigration: provider configuration changed, so migration is needed")
|
||||
return true
|
||||
}
|
||||
|
||||
// change of state_store configuration
|
||||
cachedSsVal, err := cfgState.Config(ssBackend.ConfigSchema())
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] stateStoreConfigNeedsMigration: failed to decode cached config; migration codepath must handle problem: %s", err)
|
||||
return true // let the migration codepath deal with the error
|
||||
}
|
||||
|
||||
// If we get all the way down here then it's the exact equality of the
|
||||
// two decoded values that decides our outcome. It's safe to use RawEquals
|
||||
// here (rather than Equals) because we know that unknown values can
|
||||
// never appear in backend configurations.
|
||||
if cachedSsVal.RawEquals(ssCfgVal) {
|
||||
log.Print("[TRACE] stateStoreConfigNeedsMigration: given configuration matches cached configuration, so no migration is required")
|
||||
return false
|
||||
}
|
||||
log.Print("[TRACE] stateStoreConfigNeedsMigration: configuration values have changed, so migration is required")
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Meta) stateStore_changed(cfg *configs.StateStore, cfgHash int, sMgr *clistate.LocalState, opts *BackendOpts, initReason *ssInitReason) (backend.Backend, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
vt := arguments.ViewJSON
|
||||
// Set default viewtype if none was set as the StateLocker needs to know exactly
|
||||
// what viewType we want to have.
|
||||
if opts == nil || opts.ViewType != vt {
|
||||
vt = arguments.ViewHuman
|
||||
}
|
||||
|
||||
// Get the destination state store
|
||||
dstB, storeConfigVal, providerConfigVal, moreDiags := m.stateStoreInitFromConfig(cfg, opts.Locks)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// Grab the source state store
|
||||
srcB, srcBDiags := m.savedStateStore(sMgr)
|
||||
diags = diags.Append(srcBDiags)
|
||||
if srcBDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
// Get the old state
|
||||
s := sMgr.State()
|
||||
|
||||
view := views.NewInit(vt, m.View)
|
||||
view.Output(views.StateStoreMigrationMessage,
|
||||
s.StateStore.Type, s.StateStore.Provider.Source.ForDisplay(),
|
||||
cfg.Type, cfg.ProviderAddr.ForDisplay(),
|
||||
initReason.Reason)
|
||||
|
||||
// Perform the migration
|
||||
err := m.backendMigrateState(&backendMigrateOpts{
|
||||
SourceType: s.StateStore.Type,
|
||||
DestinationType: cfg.Type,
|
||||
Source: srcB,
|
||||
Destination: dstB,
|
||||
ViewType: vt,
|
||||
})
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if m.stateLock {
|
||||
view := views.NewStateLocker(vt, m.View)
|
||||
stateLocker := clistate.NewLocker(m.stateLockTimeout, view)
|
||||
if err := stateLocker.Lock(sMgr, "state store from plan"); err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Error locking state: %s", err))
|
||||
return nil, diags
|
||||
}
|
||||
defer stateLocker.Unlock()
|
||||
}
|
||||
|
||||
var pVersion *version.Version // This will remain nil for builtin providers or unmanaged providers.
|
||||
if cfg.ProviderAddr.IsBuiltIn() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "State storage is using a builtin provider",
|
||||
Detail: "Terraform is using a builtin provider for initializing state storage. Terraform will be less able to detect when state migrations are required in future init commands.",
|
||||
})
|
||||
} else {
|
||||
isReattached, err := reattach.IsProviderReattached(cfg.ProviderAddr, os.Getenv("TF_REATTACH_PROVIDERS"))
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Unable to determine if state storage provider is reattached while initializing state store for the first time. This is a bug in Terraform and should be reported: %w", err))
|
||||
return nil, diags
|
||||
}
|
||||
if isReattached {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagWarning,
|
||||
Summary: "State storage provider is not managed by Terraform",
|
||||
Detail: "Terraform is using a provider supplied via TF_REATTACH_PROVIDERS for initializing state storage. Terraform will be less able to detect when state migrations are required in future init commands.",
|
||||
})
|
||||
} else {
|
||||
// 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 = getStateStorageProviderVersion(cfg, opts.Locks)
|
||||
diags = diags.Append(vDiags)
|
||||
if vDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the state to the new configuration
|
||||
s = sMgr.State()
|
||||
if s == nil {
|
||||
s = workdir.NewBackendStateFile()
|
||||
}
|
||||
|
||||
s.StateStore = &workdir.StateStoreConfigState{
|
||||
Type: cfg.Type,
|
||||
Hash: uint64(cfgHash),
|
||||
Provider: &workdir.ProviderConfigState{
|
||||
Source: &cfg.ProviderAddr,
|
||||
Version: pVersion,
|
||||
},
|
||||
}
|
||||
err = s.StateStore.SetConfig(storeConfigVal, dstB.ConfigSchema())
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to set state store configuration: %w", err))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
err = s.StateStore.Provider.SetConfig(providerConfigVal, dstB.ProviderSchema())
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("Failed to set state store provider configuration: %w", err))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
if err := sMgr.WriteState(s); err != nil {
|
||||
diags = diags.Append(errBackendWriteSavedDiag(err))
|
||||
return nil, diags
|
||||
}
|
||||
if err := sMgr.PersistState(); err != nil {
|
||||
diags = diags.Append(errBackendWriteSavedDiag(err))
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
return dstB, diags
|
||||
}
|
||||
|
||||
// getStateStorageProviderVersion gets the current version of the state store provider that's in use. This is achieved
|
||||
// by inspecting the current locks.
|
||||
//
|
||||
|
|
@ -2762,7 +3111,7 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V
|
|||
//
|
||||
// NOTE: the backend version of this method, `backendInitFromConfig`, prompts users for input if any required fields
|
||||
// are missing from the backend config. In `stateStoreInitFromConfig` we don't do this, and instead users will see an error.
|
||||
func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, locks *depsfile.Locks) (backend.Backend, cty.Value, cty.Value, tfdiags.Diagnostics) {
|
||||
func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, locks *depsfile.Locks) (*backendPluggable.Pluggable, cty.Value, cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
factory, pDiags := m.StateStoreProviderFactoryFromConfig(c, locks)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
|
|
@ -160,15 +161,21 @@ configuration or state have been made.`, initReason)
|
|||
)
|
||||
}
|
||||
|
||||
type ssInitReason struct {
|
||||
Reason string
|
||||
Subject *hcl.Range
|
||||
}
|
||||
|
||||
// errStateStoreInitDiag creates a diagnostic to present to users when
|
||||
// users attempt to run a non-init command after making a change to their
|
||||
// state_store configuration.
|
||||
//
|
||||
// An init reason should be provided as an argument.
|
||||
func errStateStoreInitDiag(initReason string) tfdiags.Diagnostic {
|
||||
msg := fmt.Sprintf(`Reason: %s
|
||||
func errStateStoreInitDiag(ir *ssInitReason) tfdiags.Diagnostics {
|
||||
var msg string
|
||||
if ir != nil {
|
||||
msg += fmt.Sprintf("Reason: %s\n\n", ir.Reason)
|
||||
}
|
||||
|
||||
The "state store" is the interface that Terraform uses to store state when
|
||||
msg += `The "state store" is the interface that Terraform uses to store state when
|
||||
performing operations on the local machine. If this message is showing up,
|
||||
it means that the Terraform configuration you're using is using a custom
|
||||
configuration for state storage in Terraform.
|
||||
|
|
@ -180,13 +187,27 @@ use the current configuration.
|
|||
|
||||
If the change reason above is incorrect, please verify your configuration
|
||||
hasn't changed and try again. At this point, no changes to your existing
|
||||
configuration or state have been made.`, initReason)
|
||||
configuration or state have been made.`
|
||||
|
||||
return tfdiags.Sourceless(
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if ir != nil && ir.Subject != nil {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Subject: ir.Subject,
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "State store initialization required, please run \"terraform init\"",
|
||||
Detail: msg,
|
||||
})
|
||||
return diags
|
||||
}
|
||||
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"State store initialization required, please run \"terraform init\"",
|
||||
msg,
|
||||
)
|
||||
))
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
// errBackendInitCloudDiag creates a diagnostic to present to users when
|
||||
|
|
@ -227,6 +248,23 @@ above, resolve it, and try again.`, innerError)
|
|||
)
|
||||
}
|
||||
|
||||
// errStateStoreWriteSavedDiag creates a diagnostic to present to users when
|
||||
// an init command experiences an error while writing to the backend state file.
|
||||
func errStateStoreWriteSavedDiag(innerError error) tfdiags.Diagnostic {
|
||||
msg := fmt.Sprintf(`Error saving the state store configuration: %s
|
||||
|
||||
Terraform saves the complete state store configuration in a local file for
|
||||
configuring the state store on future operations. This cannot be disabled. Errors
|
||||
are usually due to simple file permission errors. Please look at the error
|
||||
above, resolve it, and try again.`, innerError)
|
||||
|
||||
return tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"State store initialization failed",
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
// errBackendNoExistingWorkspaces is returned by calling code when it expects a backend.Backend
|
||||
// to report one or more workspaces exist.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -2657,17 +2657,10 @@ func TestMetaBackend_stateStoreInitFromConfig(t *testing.T) {
|
|||
m.testingOverrides = metaOverridesForProvider(mock)
|
||||
|
||||
// Code under test
|
||||
b, _, _, diags := m.stateStoreInitFromConfig(config, locks)
|
||||
_, _, _, diags := m.stateStoreInitFromConfig(config, locks)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors: %s", diags.Err())
|
||||
}
|
||||
if _, ok := b.(*pluggable.Pluggable); !ok {
|
||||
t.Fatalf(
|
||||
"expected stateStoreInitFromConfig to return a backend.Backend interface with concrete type %s, but got something else: %#v",
|
||||
"*pluggable.Pluggable",
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
if !mock.SetStateStoreChunkSizeCalled {
|
||||
t.Fatal("expected configuring the pluggable state store to include a call to SetStateStoreChunkSize on the provider")
|
||||
|
|
|
|||
|
|
@ -315,6 +315,10 @@ var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMe
|
|||
HumanValue: stateMigrateLocalHuman,
|
||||
JSONValue: stateMigrateLocalJSON,
|
||||
},
|
||||
"state_store_migrate_state_store": {
|
||||
HumanValue: "Migrating from state store %q (%s) to %q (%s). Reason: %s.",
|
||||
JSONValue: "Migrating from state store %q (%s) to %q (%s). Reason: %s.",
|
||||
},
|
||||
"empty_message": {
|
||||
HumanValue: "",
|
||||
JSONValue: "",
|
||||
|
|
@ -379,6 +383,8 @@ const (
|
|||
BackendMigrateStateStoreMessage InitMessageCode = "backend_migrate_state_store"
|
||||
// StateMigrateLocalMessage indicates migration from state store to local
|
||||
StateMigrateLocalMessage InitMessageCode = "state_store_migrate_local"
|
||||
// StateStoreMigrationMessage indicates migration from state store to state store
|
||||
StateStoreMigrationMessage InitMessageCode = "state_store_migrate_state_store"
|
||||
// FindingMatchingVersionMessage indicates that Terraform is looking for a provider version that matches the constraint during installation
|
||||
FindingMatchingVersionMessage InitMessageCode = "finding_matching_version_message"
|
||||
// InstalledProviderVersionInfo describes a successfully installed provider along with its version
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
// StateStore represents a "state_store" block inside a "terraform" block
|
||||
// in a module or file.
|
||||
type StateStore struct {
|
||||
// Type is a state store type name
|
||||
Type string
|
||||
|
||||
// Config is the full configuration of the state_store block, including the
|
||||
|
|
|
|||
Loading…
Reference in a new issue