Skip to content

Commit 312f296

Browse files
authored
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
1 parent 922fdb2 commit 312f296

File tree

4 files changed

+243
-2
lines changed

4 files changed

+243
-2
lines changed

internal/command/meta_backend.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/hashicorp/terraform/internal/backend/backendrun"
2828
backendInit "github.com/hashicorp/terraform/internal/backend/init"
2929
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
30+
backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable"
3031
"github.com/hashicorp/terraform/internal/cloud"
3132
"github.com/hashicorp/terraform/internal/command/arguments"
3233
"github.com/hashicorp/terraform/internal/command/clistate"
@@ -40,6 +41,7 @@ import (
4041
"github.com/hashicorp/terraform/internal/states/statemgr"
4142
"github.com/hashicorp/terraform/internal/terraform"
4243
"github.com/hashicorp/terraform/internal/tfdiags"
44+
tfversion "github.com/hashicorp/terraform/version"
4345
)
4446

4547
// BackendOpts are the options used to initialize a backendrun.OperationsBackend.
@@ -1526,6 +1528,147 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
15261528
return diags
15271529
}
15281530

1531+
// Initializing a saved state store from the backend state file (aka 'cache file', aka 'legacy state file')
1532+
func (m *Meta) savedStateStore(sMgr *clistate.LocalState, providerFactory providers.Factory) (backend.Backend, tfdiags.Diagnostics) {
1533+
// We're preparing a state_store version of backend.Backend.
1534+
//
1535+
// The provider and state store will be configured using the backend state file.
1536+
1537+
var diags tfdiags.Diagnostics
1538+
var b backend.Backend
1539+
1540+
s := sMgr.State()
1541+
1542+
provider, err := providerFactory()
1543+
if err != nil {
1544+
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
1545+
return nil, diags
1546+
}
1547+
// We purposefully don't have a deferred call to the provider's Close method here because the calling code needs a
1548+
// running provider instance inside the returned backend.Backend instance.
1549+
// Stopping the provider process is the responsibility of the calling code.
1550+
1551+
resp := provider.GetProviderSchema()
1552+
1553+
if len(resp.StateStores) == 0 {
1554+
diags = diags.Append(&hcl.Diagnostic{
1555+
Severity: hcl.DiagError,
1556+
Summary: "Provider does not support pluggable state storage",
1557+
Detail: fmt.Sprintf("There are no state stores implemented by provider %s (%q)",
1558+
s.StateStore.Provider.Source.Type,
1559+
s.StateStore.Provider.Source),
1560+
})
1561+
return nil, diags
1562+
}
1563+
1564+
stateStoreSchema, exists := resp.StateStores[s.StateStore.Type]
1565+
if !exists {
1566+
suggestions := slices.Sorted(maps.Keys(resp.StateStores))
1567+
suggestion := didyoumean.NameSuggestion(s.StateStore.Type, suggestions)
1568+
if suggestion != "" {
1569+
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
1570+
}
1571+
diags = diags.Append(&hcl.Diagnostic{
1572+
Severity: hcl.DiagError,
1573+
Summary: "State store not implemented by the provider",
1574+
Detail: fmt.Sprintf("State store %q is not implemented by provider %s (%q)%s",
1575+
s.StateStore.Type,
1576+
s.StateStore.Provider.Source.Type,
1577+
s.StateStore.Provider.Source,
1578+
suggestion),
1579+
})
1580+
return nil, diags
1581+
}
1582+
1583+
// Get the provider config from the backend state file.
1584+
providerConfigVal, err := s.StateStore.Provider.Config(resp.Provider.Body)
1585+
if err != nil {
1586+
diags = diags.Append(
1587+
&hcl.Diagnostic{
1588+
Severity: hcl.DiagError,
1589+
Summary: "Error reading provider configuration state",
1590+
Detail: fmt.Sprintf("Terraform experienced an error reading provider configuration for provider %s (%q) while configuring state store %s",
1591+
s.StateStore.Provider.Source.Type,
1592+
s.StateStore.Provider.Source,
1593+
s.StateStore.Type,
1594+
),
1595+
},
1596+
)
1597+
return nil, diags
1598+
}
1599+
1600+
// Get the state store config from the backend state file.
1601+
stateStoreConfigVal, err := s.StateStore.Config(stateStoreSchema.Body)
1602+
if err != nil {
1603+
diags = diags.Append(
1604+
&hcl.Diagnostic{
1605+
Severity: hcl.DiagError,
1606+
Summary: "Error reading state store configuration state",
1607+
Detail: fmt.Sprintf("Terraform experienced an error reading state store configuration for state store %s in provider %s (%q)",
1608+
s.StateStore.Type,
1609+
s.StateStore.Provider.Source.Type,
1610+
s.StateStore.Provider.Source,
1611+
),
1612+
},
1613+
)
1614+
return nil, diags
1615+
}
1616+
1617+
// Validate and configure the provider
1618+
//
1619+
// NOTE: there are no marks we need to remove at this point.
1620+
// We haven't added marks since the provider config from the backend state was used
1621+
// because the state-storage provider's config isn't going to be presented to the user via terminal output or diags.
1622+
validateResp := provider.ValidateProviderConfig(providers.ValidateProviderConfigRequest{
1623+
Config: providerConfigVal,
1624+
})
1625+
diags = diags.Append(validateResp.Diagnostics)
1626+
if diags.HasErrors() {
1627+
return nil, diags
1628+
}
1629+
1630+
configureResp := provider.ConfigureProvider(providers.ConfigureProviderRequest{
1631+
TerraformVersion: tfversion.SemVer.String(),
1632+
Config: providerConfigVal,
1633+
})
1634+
diags = diags.Append(configureResp.Diagnostics)
1635+
if diags.HasErrors() {
1636+
return nil, diags
1637+
}
1638+
1639+
// Validate and configure the state store
1640+
//
1641+
// NOTE: there are no marks we need to remove at this point.
1642+
// We haven't added marks since the state store config from the backend state was used
1643+
// because the state store's config isn't going to be presented to the user via terminal output or diags.
1644+
validateStoreResp := provider.ValidateStateStoreConfig(providers.ValidateStateStoreConfigRequest{
1645+
TypeName: s.StateStore.Type,
1646+
Config: stateStoreConfigVal,
1647+
})
1648+
diags = diags.Append(validateStoreResp.Diagnostics)
1649+
if diags.HasErrors() {
1650+
return nil, diags
1651+
}
1652+
1653+
cfgStoreResp := provider.ConfigureStateStore(providers.ConfigureStateStoreRequest{
1654+
TypeName: s.StateStore.Type,
1655+
Config: stateStoreConfigVal,
1656+
})
1657+
diags = diags.Append(cfgStoreResp.Diagnostics)
1658+
if diags.HasErrors() {
1659+
return nil, diags
1660+
}
1661+
1662+
// Now we have a fully configured state store, ready to be used.
1663+
// To make it usable we need to return it in a backend.Backend interface.
1664+
b, err = backendPluggable.NewPluggable(provider, s.StateStore.Type)
1665+
if err != nil {
1666+
diags = diags.Append(err)
1667+
}
1668+
1669+
return b, diags
1670+
}
1671+
15291672
//-------------------------------------------------------------------
15301673
// Reusable helper functions for backend management
15311674
//-------------------------------------------------------------------

internal/command/meta_backend_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/hashicorp/terraform/internal/addrs"
2020
"github.com/hashicorp/terraform/internal/backend"
2121
"github.com/hashicorp/terraform/internal/cloud"
22+
"github.com/hashicorp/terraform/internal/command/clistate"
2223
"github.com/hashicorp/terraform/internal/command/workdir"
2324
"github.com/hashicorp/terraform/internal/configs"
2425
"github.com/hashicorp/terraform/internal/configs/configschema"
@@ -35,7 +36,9 @@ import (
3536
"github.com/zclconf/go-cty/cty"
3637

3738
backendInit "github.com/hashicorp/terraform/internal/backend/init"
39+
"github.com/hashicorp/terraform/internal/backend/local"
3840
backendLocal "github.com/hashicorp/terraform/internal/backend/local"
41+
"github.com/hashicorp/terraform/internal/backend/pluggable"
3942
backendInmem "github.com/hashicorp/terraform/internal/backend/remote-state/inmem"
4043
)
4144

@@ -2401,6 +2404,97 @@ func TestMetaBackend_configureStateStoreVariableUse(t *testing.T) {
24012404
}
24022405
}
24032406

2407+
func TestSavedBackend(t *testing.T) {
2408+
// Create a temporary working directory
2409+
td := t.TempDir()
2410+
testCopyDir(t, testFixturePath("backend-unset"), td) // Backend state file describes local backend, config lacks backend config
2411+
t.Chdir(td)
2412+
2413+
// Make a state manager for the backend state file,
2414+
// read state from file
2415+
m := testMetaBackend(t, nil)
2416+
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
2417+
sMgr := &clistate.LocalState{Path: statePath}
2418+
err := sMgr.RefreshState()
2419+
if err != nil {
2420+
t.Fatalf("unexpected error: %s", err)
2421+
}
2422+
2423+
// Code under test
2424+
b, diags := m.savedBackend(sMgr)
2425+
if diags.HasErrors() {
2426+
t.Fatalf("unexpected errors: %s", diags.Err())
2427+
}
2428+
2429+
// The test fixtures used in this test include a backend state file describing
2430+
// a local backend with the non-default path value below (local-state.tfstate)
2431+
localB, ok := b.(*local.Local)
2432+
if !ok {
2433+
t.Fatalf("expected the returned backend to be a local backend, matching the test fixtures.")
2434+
}
2435+
if localB.StatePath != "local-state.tfstate" {
2436+
t.Fatalf("expected the local backend to be configured using the backend state file, but got unexpected configuration values.")
2437+
}
2438+
}
2439+
2440+
func TestSavedStateStore(t *testing.T) {
2441+
t.Run("the returned state store is configured with the backend state and not the current config", func(t *testing.T) {
2442+
// Create a temporary working directory
2443+
td := t.TempDir()
2444+
testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file
2445+
t.Chdir(td)
2446+
2447+
// Make a state manager for accessing the backend state file,
2448+
// and read the backend state from file
2449+
m := testMetaBackend(t, nil)
2450+
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
2451+
sMgr := &clistate.LocalState{Path: statePath}
2452+
err := sMgr.RefreshState()
2453+
if err != nil {
2454+
t.Fatalf("unexpected error: %s", err)
2455+
}
2456+
2457+
// Prepare provider factories for use
2458+
mock := testStateStoreMock(t)
2459+
factory := func() (providers.Interface, error) {
2460+
return mock, nil
2461+
}
2462+
mock.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
2463+
// Assert that the state store is configured using backend state file values from the fixtures
2464+
config := req.Config.AsValueMap()
2465+
if config["region"].AsString() != "old-value" {
2466+
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)
2467+
}
2468+
return providers.ConfigureProviderResponse{}
2469+
}
2470+
mock.ConfigureStateStoreFn = func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
2471+
// Assert that the state store is configured using backend state file values from the fixtures
2472+
config := req.Config.AsValueMap()
2473+
if config["value"].AsString() != "old-value" {
2474+
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)
2475+
}
2476+
return providers.ConfigureStateStoreResponse{}
2477+
}
2478+
2479+
// Code under test
2480+
b, diags := m.savedStateStore(sMgr, factory)
2481+
if diags.HasErrors() {
2482+
t.Fatalf("unexpected errors: %s", diags.Err())
2483+
}
2484+
2485+
if _, ok := b.(*pluggable.Pluggable); !ok {
2486+
t.Fatalf(
2487+
"expected savedStateStore to return a backend.Backend interface with concrete type %s, but got something else: %#v",
2488+
"*pluggable.Pluggable",
2489+
b,
2490+
)
2491+
}
2492+
})
2493+
2494+
// NOTE: the mock's functions include assertions about the values passed to
2495+
// the ConfigureProvider and ConfigureStateStore methods
2496+
}
2497+
24042498
func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) {
24052499
// See internal/command/e2etest/meta_backend_test.go for test case
24062500
// where a provider factory is found using a local provider cache

internal/command/testdata/state-store-changed/.terraform/terraform.tfstate

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"provider": {
1111
"version": "1.2.3",
1212
"source": "registry.terraform.io/my-org/foo",
13-
"config": {},
13+
"config": {
14+
"region": "old-value"
15+
},
1416
"hash": 12345
1517
},
1618
"hash": 12345

internal/command/testdata/state-store-changed/main.tf

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ terraform {
55
}
66
}
77
state_store "test_store" {
8-
provider "test" {}
8+
provider "test" {
9+
region = "changed-value" # changed versus backend state file
10+
}
911

1012
value = "changed-value" # changed versus backend state file
1113
}

0 commit comments

Comments
 (0)