Skip to content

Commit cf047be

Browse files
authored
PSS: Use interfaces to configure state stores (#37899)
* refactor: Move chunk size limit constants to new `chunks` package * refactor: Make `NewPluggable` return a `Pluggable` concrete type, instead of an instance of the `backend.Backend` interface. * refactor: Configure state stores through the backend.Backend interface, instead of directly using methods related to RPCs. This requires changing where we call `SetStateStoreChunkSize`. * docs: Add godocs comment to `StateStoreChunkSizeSetter` interface To summarize, we don't really need the `SetStateStoreChunkSize` method, and instead methods like `(*GRPCProvider).ConfigureStateStore` in the `plugin6` package can directly inspect the negotiation process that passes through that code and pull out the chunk size. However that means that that code would also need to implement validation. And that's just `(*GRPCProvider).ConfigureStateStore`; what about all the test mocks that are used in different types of test? They would all need to be implemented similarly to GRPCProvider to be good, useful mocks, and then a lot of things that fulfil the provider.Interface interface are coupled to each other. Instead, it's easier to have validation in the `grpcClient` struct's methods in the `remote` package, as that code is common to all scenarios. That code needs a method to 'reach into' the provider.Interface value, so we use the `SetStateStoreChunkSize` method. * chore: Make it clearer that the v6 GRPCProvider implements `SetStateStoreChunkSize` * fix: Remove unnecessary assignment of chunk size I'm surprised that removing this doesn't break E2E tests of PSS that use grpcwrap, but I think there's `plugin6` code that runs in that situation, so maybe chunking is handled elsewhere. * chore: Add panic to try detect unexpected cases when setting chunk size. * feat: Add `providers.StateStoreChunkSizeSetter` implementation to provider-simple-v6 * docs: Update code comments for NewPluggable describing its intended use
1 parent 6ed7151 commit cf047be

File tree

11 files changed

+139
-121
lines changed

11 files changed

+139
-121
lines changed

internal/backend/pluggable/chunks.go renamed to internal/backend/pluggable/chunks/chunks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) HashiCorp, Inc.
22
// SPDX-License-Identifier: BUSL-1.1
33

4-
package pluggable
4+
package chunks
55

66
const (
77
// DefaultStateStoreChunkSize is the default chunk size proposed

internal/backend/pluggable/pluggable.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ package pluggable
55

66
import (
77
"errors"
8+
"fmt"
9+
"log"
810

911
"github.com/hashicorp/terraform/internal/backend"
12+
"github.com/hashicorp/terraform/internal/backend/pluggable/chunks"
1013
"github.com/hashicorp/terraform/internal/configs/configschema"
1114
"github.com/hashicorp/terraform/internal/providers"
1215
"github.com/hashicorp/terraform/internal/states/remote"
@@ -15,19 +18,22 @@ import (
1518
"github.com/zclconf/go-cty/cty"
1619
)
1720

18-
// NewPluggable returns an instance of the backend.Backend interface that
19-
// contains a provider interface. These are the assumptions about that
20-
// provider:
21+
// NewPluggable returns a Pluggable. A Pluggable fulfils the
22+
// backend.Backend interface and allows management of state via
23+
// a state store implemented in the provider that's within the Pluggable.
2124
//
25+
// These are the assumptions about that
26+
// provider:
2227
// * The provider implements at least one state store.
23-
// * The provider has already been configured before using NewPluggable.
28+
// * The provider has already been fully configured before using NewPluggable.
2429
//
2530
// The state store could also be configured prior to using NewPluggable,
26-
// or it could be configured using the relevant backend.Backend methods.
31+
// but preferably it will be configured via the Pluggable,
32+
// using the relevant backend.Backend methods.
2733
//
2834
// By wrapping a configured provider in a Pluggable we allow calling code
2935
// to use the provider's gRPC methods when interacting with state.
30-
func NewPluggable(p providers.Interface, typeName string) (backend.Backend, error) {
36+
func NewPluggable(p providers.Interface, typeName string) (*Pluggable, error) {
3137
if p == nil {
3238
return nil, errors.New("Attempted to initialize pluggable state with a nil provider interface. This is a bug in Terraform and should be reported")
3339
}
@@ -102,14 +108,47 @@ func (p *Pluggable) PrepareConfig(config cty.Value) (cty.Value, tfdiags.Diagnost
102108
//
103109
// Configure implements backend.Backend
104110
func (p *Pluggable) Configure(config cty.Value) tfdiags.Diagnostics {
111+
var diags tfdiags.Diagnostics
105112
req := providers.ConfigureStateStoreRequest{
106113
TypeName: p.typeName,
107114
Config: config,
108115
Capabilities: providers.StateStoreClientCapabilities{
109-
ChunkSize: DefaultStateStoreChunkSize,
116+
// The core binary will always request the default chunk size from the provider to start
117+
ChunkSize: chunks.DefaultStateStoreChunkSize,
110118
},
111119
}
112120
resp := p.provider.ConfigureStateStore(req)
121+
diags = diags.Append(resp.Diagnostics)
122+
if diags.HasErrors() {
123+
return diags
124+
}
125+
126+
// Validate the returned value from chunk size negotiation
127+
chunkSize := resp.Capabilities.ChunkSize
128+
if chunkSize == 0 || chunkSize > chunks.MaxStateStoreChunkSize {
129+
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
130+
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
131+
chunks.MaxStateStoreChunkSize, chunkSize,
132+
))
133+
return diags
134+
}
135+
136+
// Negotiated chunk size is valid, so set it in the provider server
137+
// that will use the value for future RPCs to read/write state.
138+
if cs, ok := p.provider.(providers.StateStoreChunkSizeSetter); ok {
139+
cs.SetStateStoreChunkSize(p.typeName, int(chunkSize))
140+
} else {
141+
// TODO: Remove
142+
// I've put this here to try and fish for types of test setup that
143+
// use other things that should implement SetStateStoreChunkSize but
144+
// don't yet.
145+
panic("provider does not implement providers.StateStoreChunkSizeSetter interface. This is a bug in Terraform and should be reported.")
146+
}
147+
log.Printf("[TRACE] Pluggable.Configure: negotiated a chunk size of %v when configuring state store %s",
148+
chunkSize,
149+
p.typeName,
150+
)
151+
113152
return resp.Diagnostics
114153
}
115154

internal/backend/pluggable/pluggable_test.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -417,12 +417,7 @@ func TestPluggable_ProviderSchema(t *testing.T) {
417417
t.Fatalf("unexpected error: %s", err)
418418
}
419419

420-
// Calling code will need to case to Pluggable after using NewPluggable,
421-
// so we do something similar in this test
422-
var providerSchema *configschema.Block
423-
if pluggable, ok := p.(*Pluggable); ok {
424-
providerSchema = pluggable.ProviderSchema()
425-
}
420+
providerSchema := p.ProviderSchema()
426421

427422
if !mock.GetProviderSchemaCalled {
428423
t.Fatal("expected ProviderSchema to call the GetProviderSchema RPC")
@@ -450,12 +445,7 @@ func TestPluggable_ProviderSchema(t *testing.T) {
450445
t.Fatalf("unexpected error: %s", err)
451446
}
452447

453-
// Calling code will need to case to Pluggable after using NewPluggable,
454-
// so we do something similar in this test
455-
var providerSchema *configschema.Block
456-
if pluggable, ok := p.(*Pluggable); ok {
457-
providerSchema = pluggable.ProviderSchema()
458-
}
448+
providerSchema := p.ProviderSchema()
459449

460450
if !mock.GetProviderSchemaCalled {
461451
t.Fatal("expected ProviderSchema to call the GetProviderSchema RPC")

internal/command/meta_backend.go

Lines changed: 28 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,7 +1985,6 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState) (backend.Backend, tfdi
19851985
// The provider and state store will be configured using the backend state file.
19861986

19871987
var diags tfdiags.Diagnostics
1988-
var b backend.Backend
19891988

19901989
s := sMgr.State()
19911990

@@ -2092,57 +2091,24 @@ func (m *Meta) savedStateStore(sMgr *clistate.LocalState) (backend.Backend, tfdi
20922091
return nil, diags
20932092
}
20942093

2095-
// Validate and configure the state store
2096-
//
2097-
// NOTE: there are no marks we need to remove at this point.
2098-
// We haven't added marks since the state store config from the backend state was used
2099-
// because the state store's config isn't going to be presented to the user via terminal output or diags.
2100-
validateStoreResp := provider.ValidateStateStoreConfig(providers.ValidateStateStoreConfigRequest{
2101-
TypeName: s.StateStore.Type,
2102-
Config: stateStoreConfigVal,
2103-
})
2104-
diags = diags.Append(validateStoreResp.Diagnostics)
2105-
if diags.HasErrors() {
2106-
return nil, diags
2107-
}
2108-
2109-
cfgStoreResp := provider.ConfigureStateStore(providers.ConfigureStateStoreRequest{
2110-
TypeName: s.StateStore.Type,
2111-
Config: stateStoreConfigVal,
2112-
Capabilities: providers.StateStoreClientCapabilities{
2113-
ChunkSize: backendPluggable.DefaultStateStoreChunkSize,
2114-
},
2115-
})
2116-
diags = diags.Append(cfgStoreResp.Diagnostics)
2117-
if diags.HasErrors() {
2118-
return nil, diags
2119-
}
2120-
2121-
chunkSize := cfgStoreResp.Capabilities.ChunkSize
2122-
if chunkSize == 0 || chunkSize > backendPluggable.MaxStateStoreChunkSize {
2123-
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
2124-
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
2125-
backendPluggable.MaxStateStoreChunkSize, chunkSize,
2126-
))
2127-
return nil, diags
2128-
}
2129-
2130-
p, ok := provider.(providers.StateStoreChunkSizeSetter)
2131-
if !ok {
2132-
msg := fmt.Sprintf("Unable to set chunk size for provider %s; this is a bug in Terraform - please report it", s.StateStore.Type)
2133-
panic(msg)
2134-
}
2135-
// casting to int here is okay because the number should never exceed int32
2136-
p.SetStateStoreChunkSize(s.StateStore.Type, int(chunkSize))
2137-
2138-
// Now we have a fully configured state store, ready to be used.
2139-
// To make it usable we need to return it in a backend.Backend interface.
2140-
b, err = backendPluggable.NewPluggable(provider, s.StateStore.Type)
2094+
// Now that the provider is configured we can begin using the state store through
2095+
// the backend.Backend interface.
2096+
p, err := backendPluggable.NewPluggable(provider, s.StateStore.Type)
21412097
if err != nil {
21422098
diags = diags.Append(err)
21432099
}
21442100

2145-
return b, diags
2101+
// Validate and configure the state store
2102+
//
2103+
// Note: we do not use the value returned from PrepareConfig for state stores,
2104+
// however that old approach is still used with backends for compatibility reasons.
2105+
_, validateDiags := p.PrepareConfig(stateStoreConfigVal)
2106+
diags = diags.Append(validateDiags)
2107+
2108+
configureDiags := p.Configure(stateStoreConfigVal)
2109+
diags = diags.Append(configureDiags)
2110+
2111+
return p, diags
21462112
}
21472113

21482114
//-------------------------------------------------------------------
@@ -2367,58 +2333,24 @@ func (m *Meta) stateStoreInitFromConfig(c *configs.StateStore, locks *depsfile.L
23672333
return nil, cty.NilVal, cty.NilVal, diags
23682334
}
23692335

2370-
// Validate state store config and configure the state store
2371-
//
2372-
// NOTE: there are no marks we need to remove at this point.
2373-
// We haven't added marks since the provider config from the backend state was used
2374-
// because the state-storage provider's config isn't going to be presented to the user via terminal output or diags.
2375-
validateStoreResp := provider.ValidateStateStoreConfig(providers.ValidateStateStoreConfigRequest{
2376-
TypeName: c.Type,
2377-
Config: stateStoreConfigVal,
2378-
})
2379-
diags = diags.Append(validateStoreResp.Diagnostics)
2380-
if validateStoreResp.Diagnostics.HasErrors() {
2381-
return nil, cty.NilVal, cty.NilVal, diags
2382-
}
2383-
2384-
cfgStoreResp := provider.ConfigureStateStore(providers.ConfigureStateStoreRequest{
2385-
TypeName: c.Type,
2386-
Config: stateStoreConfigVal,
2387-
Capabilities: providers.StateStoreClientCapabilities{
2388-
ChunkSize: backendPluggable.DefaultStateStoreChunkSize,
2389-
},
2390-
})
2391-
diags = diags.Append(cfgStoreResp.Diagnostics)
2392-
if cfgStoreResp.Diagnostics.HasErrors() {
2393-
return nil, cty.NilVal, cty.NilVal, diags
2394-
}
2395-
2396-
chunkSize := cfgStoreResp.Capabilities.ChunkSize
2397-
if chunkSize == 0 || chunkSize > backendPluggable.MaxStateStoreChunkSize {
2398-
diags = diags.Append(fmt.Errorf("Failed to negotiate acceptable chunk size. "+
2399-
"Expected size > 0 and <= %d bytes, provider wants %d bytes",
2400-
backendPluggable.MaxStateStoreChunkSize, chunkSize,
2401-
))
2402-
return nil, cty.NilVal, cty.NilVal, diags
2403-
}
2404-
2405-
p, ok := provider.(providers.StateStoreChunkSizeSetter)
2406-
if !ok {
2407-
msg := fmt.Sprintf("Unable to set chunk size for provider %s; this is a bug in Terraform - please report it", c.Type)
2408-
panic(msg)
2409-
}
2410-
// casting to int here is okay because the number should never exceed int32
2411-
p.SetStateStoreChunkSize(c.Type, int(chunkSize))
2412-
2413-
// Now we have a fully configured state store, ready to be used.
2414-
// To make it usable we need to return it in a backend.Backend interface.
2415-
b, err := backendPluggable.NewPluggable(provider, c.Type)
2336+
// Now that the provider is configured we can begin using the state store through
2337+
// the backend.Backend interface.
2338+
p, err := backendPluggable.NewPluggable(provider, c.Type)
24162339
if err != nil {
24172340
diags = diags.Append(err)
2418-
return nil, cty.NilVal, cty.NilVal, diags
24192341
}
24202342

2421-
return b, stateStoreConfigVal, providerConfigVal, diags
2343+
// Validate and configure the state store
2344+
//
2345+
// Note: we do not use the value returned from PrepareConfig for state stores,
2346+
// however that old approach is still used with backends for compatibility reasons.
2347+
_, validateDiags := p.PrepareConfig(stateStoreConfigVal)
2348+
diags = diags.Append(validateDiags)
2349+
2350+
configureDiags := p.Configure(stateStoreConfigVal)
2351+
diags = diags.Append(configureDiags)
2352+
2353+
return p, stateStoreConfigVal, providerConfigVal, diags
24222354
}
24232355

24242356
// Helper method to get aliases from the enhanced backend and alias them

internal/grpcwrap/provider6.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import (
1818
"google.golang.org/grpc/status"
1919
"google.golang.org/protobuf/types/known/timestamppb"
2020

21+
"github.com/hashicorp/terraform/internal/backend/pluggable/chunks"
2122
proto6 "github.com/hashicorp/terraform/internal/tfplugin6"
2223

23-
backendPluggable "github.com/hashicorp/terraform/internal/backend/pluggable"
2424
"github.com/hashicorp/terraform/internal/plugin6/convert"
2525
"github.com/hashicorp/terraform/internal/providers"
2626
"github.com/hashicorp/terraform/internal/tfplugin6"
@@ -966,22 +966,21 @@ func (p *provider6) ConfigureStateStore(ctx context.Context, req *tfplugin6.Conf
966966
TypeName: req.TypeName,
967967
Config: configVal,
968968
Capabilities: providers.StateStoreClientCapabilities{
969-
ChunkSize: backendPluggable.DefaultStateStoreChunkSize,
969+
ChunkSize: chunks.DefaultStateStoreChunkSize,
970970
},
971971
})
972972

973973
// Validate the returned chunk size value
974-
if configureResp.Capabilities.ChunkSize == 0 || configureResp.Capabilities.ChunkSize > backendPluggable.MaxStateStoreChunkSize {
974+
if configureResp.Capabilities.ChunkSize == 0 || configureResp.Capabilities.ChunkSize > chunks.MaxStateStoreChunkSize {
975975
diag := &tfplugin6.Diagnostic{
976976
Severity: tfplugin6.Diagnostic_ERROR,
977977
Summary: "Failed to negotiate acceptable chunk size",
978978
Detail: fmt.Sprintf("Expected size > 0 and <= %d bytes, provider wants %d bytes",
979-
backendPluggable.MaxStateStoreChunkSize, configureResp.Capabilities.ChunkSize),
979+
chunks.MaxStateStoreChunkSize, configureResp.Capabilities.ChunkSize),
980980
}
981981
resp.Diagnostics = append(resp.Diagnostics, diag)
982982
return resp, nil
983983
}
984-
p.chunkSize = configureResp.Capabilities.ChunkSize
985984

986985
resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, configureResp.Diagnostics)
987986
resp.Capabilities = &tfplugin6.StateStoreServerCapabilities{

internal/plugin6/grpc_provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,11 @@ func (p *GRPCProvider) ConfigureStateStore(r providers.ConfigureStateStoreReques
15201520
logger.Trace("GRPCProvider.v6: ConfigureStateStore: received server capabilities", resp.Capabilities)
15211521

15221522
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
1523+
1524+
// Note: validation of chunk size will happen in the calling code, and if the data is valid
1525+
// (p *GRPCProvider) SetStateStoreChunkSize should be used to make the value accessible in
1526+
// the instance of GRPCProvider.
1527+
15231528
return resp
15241529
}
15251530

internal/plugin6/grpc_provider_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
)
3636

3737
var _ providers.Interface = (*GRPCProvider)(nil)
38+
var _ providers.StateStoreChunkSizeSetter = (*GRPCProvider)(nil) // Specific to the v6 version of GRPCProvider
3839

3940
var (
4041
equateEmpty = cmpopts.EquateEmpty()

internal/provider-simple-v6/provider.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type simple struct {
2727
fs *FsStore
2828
}
2929

30+
var _ providers.StateStoreChunkSizeSetter = &simple{}
31+
3032
// Provider returns an instance of providers.Interface
3133
func Provider() providers.Interface {
3234
return provider()
@@ -478,3 +480,14 @@ func (s simple) ValidateActionConfig(providers.ValidateActionConfigRequest) prov
478480
func (s simple) Close() error {
479481
return nil
480482
}
483+
484+
func (s simple) SetStateStoreChunkSize(typeName string, size int) {
485+
switch typeName {
486+
case inMemStoreName:
487+
s.inMem.SetStateStoreChunkSize(typeName, size)
488+
case fsStoreName:
489+
s.fs.SetStateStoreChunkSize(typeName, size)
490+
default:
491+
panic("SetStateStoreChunkSize called with unrecognized state store type name.")
492+
}
493+
}

internal/provider-simple-v6/state_store_fs.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type FsStore struct {
3838
states map[string]*statemgr.Filesystem
3939
}
4040

41+
var _ providers.StateStoreChunkSizeSetter = &FsStore{}
42+
4143
func stateStoreFsGetSchema() providers.Schema {
4244
return providers.Schema{
4345
Body: &configschema.Block{
@@ -261,3 +263,15 @@ func (f *FsStore) WriteStateBytes(req providers.WriteStateBytesRequest) provider
261263

262264
return resp
263265
}
266+
267+
func (f *FsStore) SetStateStoreChunkSize(typeName string, size int) {
268+
if typeName != fsStoreName {
269+
// If we hit this code it suggests someone's refactoring the PSS implementations used for testing
270+
panic(fmt.Sprintf("calling code tried to set the state store size on %s state store but the request reached the %s store implementation.",
271+
typeName,
272+
fsStoreName,
273+
))
274+
}
275+
276+
f.chunkSize = int64(size)
277+
}

0 commit comments

Comments
 (0)