Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions internal/command/meta_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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
//-------------------------------------------------------------------
Expand Down
94 changes: 94 additions & 0 deletions internal/command/meta_backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion internal/command/testdata/state-store-changed/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down