Skip to content

Commit 17445f6

Browse files
PSS: Add chunk size negotiation to savedStateStore method, update mock provider for tests. (#37726)
* Add SetStateStoreChunkSize to the mock provider for tests * Implement configurable state chunk size * Add chunk size negotiation to `savedStateStore`, update happy path test to assert it's set * Update `savedStateStore` to return diagnostic if a nil factory is passed in, add unhappy path tests * Fix test error message * Apply suggestions from code review Co-authored-by: Radek Simko <[email protected]> * Fix rename --------- Co-authored-by: Radek Simko <[email protected]> Co-authored-by: Radek Simko <[email protected]>
1 parent 62540da commit 17445f6

File tree

3 files changed

+171
-7
lines changed

3 files changed

+171
-7
lines changed

internal/command/meta_backend.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ import (
4444
tfversion "github.com/hashicorp/terraform/version"
4545
)
4646

47+
const (
48+
// defaultStateStoreChunkSize is the default chunk size proposed
49+
// to the provider.
50+
// This can be tweaked but should provide reasonable performance
51+
// trade-offs for average network conditions and state file sizes.
52+
defaultStateStoreChunkSize int64 = 8 << 20 // 8 MB
53+
54+
// maxStateStoreChunkSize is the highest chunk size provider may choose
55+
// which we still consider reasonable/safe.
56+
// This reflects terraform-plugin-go's max. RPC message size of 256MB
57+
// and leaves plenty of space for other variable data like diagnostics.
58+
maxStateStoreChunkSize int64 = 128 << 20 // 128 MB
59+
)
60+
4761
// BackendOpts are the options used to initialize a backendrun.OperationsBackend.
4862
type BackendOpts struct {
4963
// BackendConfig is a representation of the backend configuration block given in
@@ -1529,17 +1543,23 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi
15291543
}
15301544

15311545
// 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) {
1546+
func (m *Meta) savedStateStore(sMgr *clistate.LocalState, factory providers.Factory) (backend.Backend, tfdiags.Diagnostics) {
15331547
// We're preparing a state_store version of backend.Backend.
15341548
//
15351549
// The provider and state store will be configured using the backend state file.
15361550

15371551
var diags tfdiags.Diagnostics
15381552
var b backend.Backend
15391553

1540-
s := sMgr.State()
1541-
1542-
provider, err := providerFactory()
1554+
if factory == nil {
1555+
diags = diags.Append(&hcl.Diagnostic{
1556+
Severity: hcl.DiagError,
1557+
Summary: "Missing provider details when configuring state store",
1558+
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.",
1559+
})
1560+
return nil, diags
1561+
}
1562+
provider, err := factory()
15431563
if err != nil {
15441564
diags = diags.Append(fmt.Errorf("error when obtaining provider instance during state store initialization: %w", err))
15451565
return nil, diags
@@ -1548,6 +1568,7 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState, providerFactory provid
15481568
// running provider instance inside the returned backend.Backend instance.
15491569
// Stopping the provider process is the responsibility of the calling code.
15501570

1571+
s := sMgr.State()
15511572
resp := provider.GetProviderSchema()
15521573

15531574
if len(resp.StateStores) == 0 {
@@ -1653,12 +1674,32 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState, providerFactory provid
16531674
cfgStoreResp := provider.ConfigureStateStore(providers.ConfigureStateStoreRequest{
16541675
TypeName: s.StateStore.Type,
16551676
Config: stateStoreConfigVal,
1677+
Capabilities: providers.StateStoreClientCapabilities{
1678+
ChunkSize: defaultStateStoreChunkSize,
1679+
},
16561680
})
16571681
diags = diags.Append(cfgStoreResp.Diagnostics)
16581682
if diags.HasErrors() {
16591683
return nil, diags
16601684
}
16611685

1686+
chunkSize := cfgStoreResp.Capabilities.ChunkSize
1687+
if chunkSize == 0 || chunkSize > maxStateStoreChunkSize {
1688+
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
1689+
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
1690+
maxStateStoreChunkSize, chunkSize,
1691+
))
1692+
return nil, diags
1693+
}
1694+
1695+
p, ok := provider.(providers.StateStoreChunkSizeSetter)
1696+
if !ok {
1697+
msg := fmt.Sprintf("Unable to set chunk size for provider %s; this is a bug in Terraform - please report it", s.StateStore.Type)
1698+
panic(msg)
1699+
}
1700+
// casting to int here is okay because the number should never exceed int32
1701+
p.SetStateStoreChunkSize(s.StateStore.Type, int(chunkSize))
1702+
16621703
// Now we have a fully configured state store, ready to be used.
16631704
// To make it usable we need to return it in a backend.Backend interface.
16641705
b, err = backendPluggable.NewPluggable(provider, s.StateStore.Type)

internal/command/meta_backend_test.go

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,6 +2440,7 @@ func TestSavedBackend(t *testing.T) {
24402440
func TestSavedStateStore(t *testing.T) {
24412441
t.Run("the returned state store is configured with the backend state and not the current config", func(t *testing.T) {
24422442
// Create a temporary working directory
2443+
chunkSize := 42
24432444
td := t.TempDir()
24442445
testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file
24452446
t.Chdir(td)
@@ -2473,7 +2474,21 @@ func TestSavedStateStore(t *testing.T) {
24732474
if config["value"].AsString() != "old-value" {
24742475
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)
24752476
}
2476-
return providers.ConfigureStateStoreResponse{}
2477+
return providers.ConfigureStateStoreResponse{
2478+
Capabilities: providers.StateStoreServerCapabilities{
2479+
ChunkSize: int64(chunkSize),
2480+
},
2481+
}
2482+
}
2483+
mock.SetStateStoreChunkSizeFn = func(storeType string, size int) {
2484+
if storeType != "test_store" || size != chunkSize {
2485+
t.Fatalf("expected SetStateStoreChunkSize to be passed store type %q and chunk size %v, but got %q and %v",
2486+
"test_store",
2487+
chunkSize,
2488+
storeType,
2489+
size,
2490+
)
2491+
}
24772492
}
24782493

24792494
// Code under test
@@ -2489,10 +2504,104 @@ func TestSavedStateStore(t *testing.T) {
24892504
b,
24902505
)
24912506
}
2507+
2508+
if !mock.SetStateStoreChunkSizeCalled {
2509+
t.Fatal("expected configuring the pluggable state store to include a call to SetStateStoreChunkSize on the provider")
2510+
}
2511+
})
2512+
2513+
t.Run("error - no provider factory", func(t *testing.T) {
2514+
// sMgr pointing to a file that doesn't exist is sufficient setup for this test
2515+
sMgr := &clistate.LocalState{Path: "foobar.tfstate"}
2516+
2517+
m := testMetaBackend(t, nil)
2518+
_, diags := m.savedStateStore(sMgr, nil)
2519+
if !diags.HasErrors() {
2520+
t.Fatal("expected errors but got none")
2521+
}
2522+
2523+
expectedErr := "Missing provider details when configuring state store"
2524+
if !strings.Contains(diags.Err().Error(), expectedErr) {
2525+
t.Fatalf("expected the returned error to include %q, got: %s",
2526+
expectedErr,
2527+
diags.Err(),
2528+
)
2529+
}
2530+
})
2531+
2532+
t.Run("error - when there's no state stores in provider", func(t *testing.T) {
2533+
// Create a temporary working directory
2534+
td := t.TempDir()
2535+
testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file
2536+
t.Chdir(td)
2537+
2538+
// Make a state manager for accessing the backend state file,
2539+
// and read the backend state from file
2540+
m := testMetaBackend(t, nil)
2541+
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
2542+
sMgr := &clistate.LocalState{Path: statePath}
2543+
err := sMgr.RefreshState()
2544+
if err != nil {
2545+
t.Fatalf("unexpected error: %s", err)
2546+
}
2547+
2548+
mock := testStateStoreMock(t)
2549+
delete(mock.GetProviderSchemaResponse.StateStores, "test_store") // Remove the only state store impl.
2550+
2551+
_, diags := m.savedStateStore(sMgr, providers.FactoryFixed(mock))
2552+
if !diags.HasErrors() {
2553+
t.Fatal("expected errors but got none")
2554+
}
2555+
expectedErr := "Provider does not support pluggable state storage"
2556+
if !strings.Contains(diags.Err().Error(), expectedErr) {
2557+
t.Fatalf("expected the returned error to include %q, got: %s",
2558+
expectedErr,
2559+
diags.Err(),
2560+
)
2561+
}
24922562
})
24932563

2494-
// NOTE: the mock's functions include assertions about the values passed to
2495-
// the ConfigureProvider and ConfigureStateStore methods
2564+
t.Run("error - when there's no matching state store in provider Terraform suggests different identifier", func(t *testing.T) {
2565+
// Create a temporary working directory
2566+
td := t.TempDir()
2567+
testCopyDir(t, testFixturePath("state-store-changed"), td) // Fixtures with config that differs from backend state file
2568+
t.Chdir(td)
2569+
2570+
// Make a state manager for accessing the backend state file,
2571+
// and read the backend state from file
2572+
m := testMetaBackend(t, nil)
2573+
statePath := filepath.Join(m.DataDir(), DefaultStateFilename)
2574+
sMgr := &clistate.LocalState{Path: statePath}
2575+
err := sMgr.RefreshState()
2576+
if err != nil {
2577+
t.Fatalf("unexpected error: %s", err)
2578+
}
2579+
2580+
mock := testStateStoreMock(t)
2581+
testStore := mock.GetProviderSchemaResponse.StateStores["test_store"]
2582+
delete(mock.GetProviderSchemaResponse.StateStores, "test_store")
2583+
// Make the provider contain a "test_bore" impl., while the config specifies a "test_store" impl.
2584+
mock.GetProviderSchemaResponse.StateStores["test_bore"] = testStore
2585+
2586+
_, diags := m.savedStateStore(sMgr, providers.FactoryFixed(mock))
2587+
if !diags.HasErrors() {
2588+
t.Fatal("expected errors but got none")
2589+
}
2590+
expectedErr := "State store not implemented by the provider"
2591+
if !strings.Contains(diags.Err().Error(), expectedErr) {
2592+
t.Fatalf("expected the returned error to include %q, got: %s",
2593+
expectedErr,
2594+
diags.Err(),
2595+
)
2596+
}
2597+
expectedMsg := `Did you mean "test_bore"?`
2598+
if !strings.Contains(diags.Err().Error(), expectedMsg) {
2599+
t.Fatalf("expected the returned error to include %q, got: %s",
2600+
expectedMsg,
2601+
diags.Err(),
2602+
)
2603+
}
2604+
})
24962605
}
24972606

24982607
func TestMetaBackend_GetStateStoreProviderFactory(t *testing.T) {

internal/providers/testing/provider_mock.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
)
1717

1818
var _ providers.Interface = (*MockProvider)(nil)
19+
var _ providers.StateStoreChunkSizeSetter = (*MockProvider)(nil)
1920

2021
// MockProvider implements providers.Interface but mocks out all the
2122
// calls for testing purposes.
@@ -156,6 +157,10 @@ type MockProvider struct {
156157
WriteStateBytesFn func(providers.WriteStateBytesRequest) providers.WriteStateBytesResponse
157158
WriteStateBytesResponse providers.WriteStateBytesResponse
158159

160+
// See providers.StateStoreChunkSizeSetter interface
161+
SetStateStoreChunkSizeCalled bool
162+
SetStateStoreChunkSizeFn func(string, int)
163+
159164
LockStateCalled bool
160165
LockStateResponse providers.LockStateResponse
161166
LockStateRequest providers.LockStateRequest
@@ -1181,3 +1186,12 @@ func (p *MockProvider) ValidateActionConfig(r providers.ValidateActionConfigRequ
11811186

11821187
return resp
11831188
}
1189+
1190+
func (p *MockProvider) SetStateStoreChunkSize(storeType string, chunkSize int) {
1191+
p.SetStateStoreChunkSizeCalled = true
1192+
if p.SetStateStoreChunkSizeFn != nil {
1193+
p.SetStateStoreChunkSizeFn(storeType, chunkSize)
1194+
}
1195+
1196+
// If there's no function to use above we do nothing
1197+
}

0 commit comments

Comments
 (0)