PSS: Add `savedStateStore` method to `Meta` (#37558)

* Add test coverage for Meta's `savedBackend` method

* Add new Meta `savedStateStore` method and test coverage

* Streamline test - remove unneeded assertions and update comments

* Remove marks from config before configuring the provider

* Remove marks from config before configuring the state store

* Add test case for savedStateStore to assert marks aren't passed

* Fix call to ConfigureStateStore

* Show that tests pass despite not trying to remove marks

* Allow Config methods to add marks when reading pluggable state store config from the backend state file

* This code is now necessary to let the tests pass

* Stop adding marks to PSS-related config when it's parsed from the backend state file

* Stop removing marks that aren't there

* Remove unnecessary test related to marks
pull/37724/head
Sarah French 8 months ago committed by GitHub
parent 922fdb2382
commit 312f296c2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -27,6 +27,7 @@ import (
"github.com/hashicorp/terraform/internal/backend/backendrun"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable"
"github.com/hashicorp/terraform/internal/cloud"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/clistate"
@ -40,6 +41,7 @@ import (
"github.com/hashicorp/terraform/internal/states/statemgr"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
)
// BackendOpts are the options used to initialize a backendrun.OperationsBackend.
@ -1526,6 +1528,147 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
return diags
}
// Initializing a saved state store from the backend state file (aka 'cache file', aka 'legacy state file')
func (m *Meta) savedStateStore(sMgr *clistate.LocalState, providerFactory providers.Factory) (backend.Backend, tfdiags.Diagnostics) {
// We're preparing a state_store version of backend.Backend.
//
// The provider and state store will be configured using the backend state file.
var diags tfdiags.Diagnostics
var b backend.Backend
s := sMgr.State()
provider, err := providerFactory()
if err != nil {
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
return nil, diags
}
// We purposefully don't have a deferred call to the provider's Close method here because the calling code needs a
// running provider instance inside the returned backend.Backend instance.
// Stopping the provider process is the responsibility of the calling code.
resp := provider.GetProviderSchema()
if len(resp.StateStores) == 0 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Provider does not support pluggable state storage",
Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)",
s.StateStore.Provider.Source.Type,
s.StateStore.Provider.Source),
})
return nil, diags
}
stateStoreSchema, exists := resp.StateStores[s.StateStore.Type]
if !exists {
suggestions := slices.Sorted(maps.Keys(resp.StateStores))
suggestion := didyoumean.NameSuggestion(s.StateStore.Type, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "State store not implemented by the provider",
Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s",
s.StateStore.Type,
s.StateStore.Provider.Source.Type,
s.StateStore.Provider.Source,
suggestion),
})
return nil, diags
}
// Get the provider config from the backend state file.
providerConfigVal, err := s.StateStore.Provider.Config(resp.Provider.Body)
if err != nil {
diags = diags.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error reading provider configuration state",
Detail: fmt.Sprintf("Terraform experienced an error reading provider configuration for provider %s (%q) while configuring state store %s",
s.StateStore.Provider.Source.Type,
s.StateStore.Provider.Source,
s.StateStore.Type,
),
},
)
return nil, diags
}
// Get the state store config from the backend state file.
stateStoreConfigVal, err := s.StateStore.Config(stateStoreSchema.Body)
if err != nil {
diags = diags.Append(
&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Error reading state store configuration state",
Detail: fmt.Sprintf("Terraform experienced an error reading state store configuration for state store %s in provider %s (%q)",
s.StateStore.Type,
s.StateStore.Provider.Source.Type,
s.StateStore.Provider.Source,
),
},
)
return nil, diags
}
// Validate and configure the provider
//
// NOTE: there are no marks we need to remove at this point.
// We haven't added marks since the provider config from the backend state was used
// because the state-storage provider's config isn't going to be presented to the user via terminal output or diags.
validateResp := provider.ValidateProviderConfig(providers.ValidateProviderConfigRequest{
Config: providerConfigVal,
})
diags = diags.Append(validateResp.Diagnostics)
if diags.HasErrors() {
return nil, diags
}
configureResp := provider.ConfigureProvider(providers.ConfigureProviderRequest{
TerraformVersion: tfversion.SemVer.String(),
Config: providerConfigVal,
})
diags = diags.Append(configureResp.Diagnostics)
if diags.HasErrors() {
return nil, diags
}
// Validate and configure the state store
//
// NOTE: there are no marks we need to remove at this point.
// We haven't added marks since the state store config from the backend state was used
// because the state store's config isn't going to be presented to the user via terminal output or diags.
validateStoreResp := provider.ValidateStateStoreConfig(providers.ValidateStateStoreConfigRequest{
TypeName: s.StateStore.Type,
Config: stateStoreConfigVal,
})
diags = diags.Append(validateStoreResp.Diagnostics)
if diags.HasErrors() {
return nil, diags
}
cfgStoreResp := provider.ConfigureStateStore(providers.ConfigureStateStoreRequest{
TypeName: s.StateStore.Type,
Config: stateStoreConfigVal,
})
diags = diags.Append(cfgStoreResp.Diagnostics)
if diags.HasErrors() {
return nil, diags
}
// Now we have a fully configured state store, ready to be used.
// To make it usable we need to return it in a backend.Backend interface.
b, err = backendPluggable.NewPluggable(provider, s.StateStore.Type)
if err != nil {
diags = diags.Append(err)
}
return b, diags
}
//-------------------------------------------------------------------
// Reusable helper functions for backend management
//-------------------------------------------------------------------

@ -19,6 +19,7 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/cloud"
"github.com/hashicorp/terraform/internal/command/clistate"
"github.com/hashicorp/terraform/internal/command/workdir"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/configs/configschema"
@ -35,7 +36,9 @@ import (
"github.com/zclconf/go-cty/cty"
backendInit "github.com/hashicorp/terraform/internal/backend/init"
"github.com/hashicorp/terraform/internal/backend/local"
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
"github.com/hashicorp/terraform/internal/backend/pluggable"
backendInmem "github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
)
@ -2401,6 +2404,97 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) {
}
}
func TestSavedBackend(t *testing.T) {
// Create a temporary working directory
td := t.TempDir()
testCopyDir(t, testFixturePath("backend-unset"), td) // Backend state file describes local backend, config lacks backend config
t.Chdir(td)
// Make a state manager for the backend state file,
// read state from file
m := testMetaBackend(t, nil)
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
err := sMgr.RefreshState()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// Code under test
b, diags := m.savedBackend(sMgr)
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
// The test fixtures used in this test include a backend state file describing
// a local backend with the non-default path value below (local-state.tfstate)
localB, ok := b.(*local.Local)
if !ok {
t.Fatalf("expected the returned backend to be a local backend, matching the test fixtures.")
}
if localB.StatePath != "local-state.tfstate" {
t.Fatalf("expected the local backend to be configured using the backend state file, but got unexpected configuration values.")
}
}
func TestSavedStateStore(t *testing.T) {
t.Run("the returned state store is configured with the backend state and not the current config", 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
t.Chdir(td)
// Make a state manager for accessing the backend state file,
// and read the backend state from file
m := testMetaBackend(t, nil)
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
sMgr := &clistate.LocalState{Path: statePath}
err := sMgr.RefreshState()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
// Prepare provider factories for use
mock := testStateStoreMock(t)
factory := func() (providers.Interface, error) {
return mock, nil
}
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)
}
return providers.ConfigureProviderResponse{}
}
mock.ConfigureStateStoreFn = func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
// Assert that the state store is configured using backend state file values from the fixtures
config := req.Config.AsValueMap()
if config["value"].AsString() != "old-value" {
t.Fatalf("expected the state store to be configured with values from the backend state file (the string \"old-value\"), not the config. Got: %#v", config)
}
return providers.ConfigureStateStoreResponse{}
}
// Code under test
b, diags := m.savedStateStore(sMgr, factory)
if diags.HasErrors() {
t.Fatalf("unexpected errors: %s", diags.Err())
}
if _, ok := b.(*pluggable.Pluggable); !ok {
t.Fatalf(
"expected savedStateStore to return a backend.Backend interface with concrete type %s, but got something else: %#v",
"*pluggable.Pluggable",
b,
)
}
})
// NOTE: the mock's functions include assertions about the values passed to
// the ConfigureProvider and ConfigureStateStore methods
}
func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) {
// See internal/command/e2etest/meta_backend_test.go for test case
// where a provider factory is found using a local provider cache

@ -10,7 +10,9 @@
"provider": {
"version": "1.2.3",
"source": "registry.terraform.io/my-org/foo",
"config": {},
"config": {
"region": "old-value"
},
"hash": 12345
},
"hash": 12345

@ -5,7 +5,9 @@ terraform {
}
}
state_store "test_store" {
provider "test" {}
provider "test" {
region = "changed-value" # changed versus backend state file
}
value = "changed-value" # changed versus backend state file
}

Loading…
Cancel
Save