Skip to content

Commit 4746af9

Browse files
statestore: Implement configuring of state store + chunk size negotiation (#1262)
* Templating out Framework implementation for Pluggable State Storage * Updating commit * Saving my progress, todo: add clientcapabilities to plugin go * Updated read to be similar to invoke actions and handle streaming in protov6server * Updated go mod * updating some chunking logic * Added chunking to readstatebytes * ran linter * Fixed test to pass CI possibly * updating getstatestore * updated other tests * remove files unrelated to schema/metadata * add stub impl for state store rpcs * doc fixes + field updates to match proposed protocol * update copyright headers * finish impl for get metadata and get provider schema * add validation error for state stores in v5 provider * err msg fixes * update to use main branch commit * initial impl, no package docs or protov6server/fwserver tests * rename to initialize * add more tests * add package docs and move client capability to internal --------- Co-authored-by: Rain <rainskwan@gmail.com> Co-authored-by: Rain Kwan <91649079+rainkwan@users.noreply.github.com>
1 parent b1bdca1 commit 4746af9

22 files changed

+1087
-7
lines changed

internal/fromproto6/client_capabilities.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/hashicorp/terraform-plugin-framework/action"
1010
"github.com/hashicorp/terraform-plugin-framework/datasource"
1111
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
1213
"github.com/hashicorp/terraform-plugin-framework/provider"
1314
"github.com/hashicorp/terraform-plugin-framework/resource"
1415
)
@@ -116,3 +117,17 @@ func ModifyPlanActionClientCapabilities(in *tfprotov6.PlanActionClientCapabiliti
116117
DeferralAllowed: in.DeferralAllowed,
117118
}
118119
}
120+
121+
func ConfigureStateStoreClientCapabilities(in *tfprotov6.ConfigureStateStoreClientCapabilities) fwserver.ConfigureStateStoreClientCapabilities {
122+
if in == nil {
123+
// Client did not indicate any supported capabilities, in practice this shouldn't happen, but if it does
124+
// we'll just default to the same chunk size that Terraform core does, 8MB.
125+
return fwserver.ConfigureStateStoreClientCapabilities{
126+
ChunkSize: 8 << 20,
127+
}
128+
}
129+
130+
return fwserver.ConfigureStateStoreClientCapabilities{
131+
ChunkSize: in.ChunkSize,
132+
}
133+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright IBM Corp. 2021, 2025
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fromproto6
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
13+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
14+
"github.com/hashicorp/terraform-plugin-framework/statestore"
15+
)
16+
17+
// ConfigureStateStoreRequest returns the *fwserver.ConfigureStateStoreRequest
18+
// equivalent of a *tfprotov6.ConfigureStateStoreRequest.
19+
func ConfigureStateStoreRequest(ctx context.Context, proto6 *tfprotov6.ConfigureStateStoreRequest, reqStateStore statestore.StateStore, stateStoreSchema fwschema.Schema) (*fwserver.ConfigureStateStoreRequest, diag.Diagnostics) {
20+
if proto6 == nil {
21+
return nil, nil
22+
}
23+
24+
fw := &fwserver.ConfigureStateStoreRequest{
25+
StateStore: reqStateStore,
26+
StateStoreSchema: stateStoreSchema,
27+
ClientCapabilities: ConfigureStateStoreClientCapabilities(proto6.Capabilities),
28+
}
29+
30+
config, diags := Config(ctx, proto6.Config, stateStoreSchema)
31+
fw.Config = config
32+
33+
return fw, diags
34+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright IBM Corp. 2021, 2025
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fromproto6_test
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/google/go-cmp/cmp"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/fromproto6"
13+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
14+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
15+
"github.com/hashicorp/terraform-plugin-framework/statestore"
16+
"github.com/hashicorp/terraform-plugin-framework/statestore/schema"
17+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
18+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
19+
"github.com/hashicorp/terraform-plugin-go/tftypes"
20+
)
21+
22+
func TestConfigureStateStoreRequest(t *testing.T) {
23+
t.Parallel()
24+
25+
testProto6Type := tftypes.Object{
26+
AttributeTypes: map[string]tftypes.Type{
27+
"test_attribute": tftypes.String,
28+
},
29+
}
30+
31+
testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{
32+
"test_attribute": tftypes.NewValue(tftypes.String, "test-value"),
33+
})
34+
35+
testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value)
36+
37+
if err != nil {
38+
t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err)
39+
}
40+
41+
testFwSchema := schema.Schema{
42+
Attributes: map[string]schema.Attribute{
43+
"test_attribute": schema.StringAttribute{
44+
Required: true,
45+
},
46+
},
47+
}
48+
49+
defaultClientCapabilities := fwserver.ConfigureStateStoreClientCapabilities{
50+
ChunkSize: 8 << 20,
51+
}
52+
53+
testCases := map[string]struct {
54+
input *tfprotov6.ConfigureStateStoreRequest
55+
stateStoreSchema fwschema.Schema
56+
stateStoreImpl statestore.StateStore
57+
expected *fwserver.ConfigureStateStoreRequest
58+
expectedDiagnostics diag.Diagnostics
59+
}{
60+
"nil": {
61+
input: nil,
62+
expected: nil,
63+
},
64+
"empty": {
65+
input: &tfprotov6.ConfigureStateStoreRequest{},
66+
expected: &fwserver.ConfigureStateStoreRequest{
67+
ClientCapabilities: defaultClientCapabilities,
68+
},
69+
},
70+
"config-missing-schema": {
71+
input: &tfprotov6.ConfigureStateStoreRequest{
72+
Config: &testProto6DynamicValue,
73+
},
74+
expected: &fwserver.ConfigureStateStoreRequest{
75+
ClientCapabilities: defaultClientCapabilities,
76+
},
77+
expectedDiagnostics: diag.Diagnostics{
78+
diag.NewErrorDiagnostic(
79+
"Unable to Convert Configuration",
80+
"An unexpected error was encountered when converting the configuration from the protocol type. "+
81+
"This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+
82+
"Please report this to the provider developer:\n\n"+
83+
"Missing schema.",
84+
),
85+
},
86+
},
87+
"config": {
88+
input: &tfprotov6.ConfigureStateStoreRequest{
89+
Config: &testProto6DynamicValue,
90+
},
91+
stateStoreSchema: testFwSchema,
92+
expected: &fwserver.ConfigureStateStoreRequest{
93+
Config: &tfsdk.Config{
94+
Raw: testProto6Value,
95+
Schema: testFwSchema,
96+
},
97+
StateStoreSchema: testFwSchema,
98+
ClientCapabilities: defaultClientCapabilities,
99+
},
100+
},
101+
"client-capability": {
102+
input: &tfprotov6.ConfigureStateStoreRequest{
103+
Capabilities: &tfprotov6.ConfigureStateStoreClientCapabilities{
104+
ChunkSize: 4 << 20,
105+
},
106+
},
107+
stateStoreSchema: testFwSchema,
108+
expected: &fwserver.ConfigureStateStoreRequest{
109+
StateStoreSchema: testFwSchema,
110+
ClientCapabilities: fwserver.ConfigureStateStoreClientCapabilities{
111+
ChunkSize: 4 << 20,
112+
},
113+
},
114+
},
115+
"client-capability-unset": {
116+
input: &tfprotov6.ConfigureStateStoreRequest{},
117+
stateStoreSchema: testFwSchema,
118+
expected: &fwserver.ConfigureStateStoreRequest{
119+
StateStoreSchema: testFwSchema,
120+
ClientCapabilities: defaultClientCapabilities,
121+
},
122+
},
123+
}
124+
125+
for name, testCase := range testCases {
126+
t.Run(name, func(t *testing.T) {
127+
t.Parallel()
128+
129+
got, diags := fromproto6.ConfigureStateStoreRequest(context.Background(), testCase.input, testCase.stateStoreImpl, testCase.stateStoreSchema)
130+
131+
if diff := cmp.Diff(got, testCase.expected); diff != "" {
132+
t.Errorf("unexpected difference: %s", diff)
133+
}
134+
135+
if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" {
136+
t.Errorf("unexpected diagnostics difference: %s", diff)
137+
}
138+
})
139+
}
140+
}

internal/fwserver/server.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ type Server struct {
5353
// to [action.ConfigureRequest.ProviderData].
5454
ActionConfigureData any
5555

56+
// StateStoreProviderData is provider-defined data, clients, etc. that is
57+
// passed to [statestore.InitializeRequest.ProviderData].
58+
//
59+
// As state stores have a dedicated ConfigureStateStore RPC with their
60+
// own configuration to consume, this value is not passed to [statestore.ConfigureRequest.StateStoreData]
61+
// automatically, but must be explicitly set to [statestore.InitializeResponse.StateStoreData].
62+
StateStoreProviderData any
63+
64+
// StateStoreConfigureData is configured data from [statestore.InitializeResponse.StateStoreData]
65+
// and the determined server capabilities (returned from ConfigureStateStore RPC).
66+
//
67+
// The configured data should be used to populate [statestore.ConfigureRequest.StateStoreData] prior to executing
68+
// any [statestore.StateStore] methods, and the server capabilities should be used to receive/send the right chunk sizes during
69+
// the ReadStateBytes and WriteStateBytes RPCs.
70+
//
71+
// MAINTAINER NOTE: While it's possible for a provider to contain multiple state store implementations, it's not possible
72+
// for a Terraform configuration to use multiple state stores simultaneously, so it's safe to only store a single field of
73+
// configure data for the entire provider.
74+
StateStoreConfigureData StateStoreConfigureData
75+
5676
// actionSchemas is the cached Action Schemas for RPCs that need to
5777
// convert configuration data from the protocol. If not found, it will be
5878
// fetched from the Action.Schema() method.

internal/fwserver/server_capabilities.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ func (s *Server) ServerCapabilities() *ServerCapabilities {
3838
PlanDestroy: true,
3939
}
4040
}
41+
42+
// StateStoreServerCapabilities is internal to fwserver as we don't need to expose it to state store implementations currently.
43+
type StateStoreServerCapabilities struct {
44+
// ChunkSize is the provider-chosen size of state byte chunks that will be sent between Terraform and
45+
// the provider in the ReadStateBytes and WriteStateBytes RPC calls.
46+
//
47+
// As we don't expose this to providers during ConfigureStateStore currently, the provider-chosen size will always be
48+
// the Terraform core defaulted value (8 MB).
49+
ChunkSize int64
50+
}

internal/fwserver/server_configureprovider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ func (s *Server) ConfigureProvider(ctx context.Context, req *provider.ConfigureR
4040
s.EphemeralResourceConfigureData = resp.EphemeralResourceData
4141
s.ActionConfigureData = resp.ActionData
4242
s.ListResourceConfigureData = resp.ListResourceData
43+
s.StateStoreProviderData = resp.StateStoreData
4344
}

internal/fwserver/server_configureprovider_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,20 @@ func TestServerConfigureProvider(t *testing.T) {
220220
ListResourceData: "test-provider-configure-value",
221221
},
222222
},
223+
"response-statestore-data": {
224+
server: &fwserver.Server{
225+
Provider: &testprovider.Provider{
226+
SchemaMethod: func(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {},
227+
ConfigureMethod: func(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
228+
resp.StateStoreData = "test-provider-configure-value"
229+
},
230+
},
231+
},
232+
request: &provider.ConfigureRequest{},
233+
expectedResponse: &provider.ConfigureResponse{
234+
StateStoreData: "test-provider-configure-value",
235+
},
236+
},
223237
"response-invalid-deferral-diagnostic": {
224238
server: &fwserver.Server{
225239
Provider: &testprovider.Provider{
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright IBM Corp. 2021, 2025
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fwserver
5+
6+
import (
7+
"context"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
11+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
12+
"github.com/hashicorp/terraform-plugin-framework/statestore"
13+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
14+
"github.com/hashicorp/terraform-plugin-go/tftypes"
15+
)
16+
17+
// MAINTAINER NOTE: Currently, we just round-trip the proposed chunk size from Terraform core (8 MB). In the future,
18+
// we could expose this to provider developers in [statestore.InitializeResponse] if controlling
19+
// the chunk size is desired.
20+
type ConfigureStateStoreClientCapabilities struct {
21+
// ChunkSize is the client-requested size of state byte chunks that are sent between Terraform Core and
22+
// The default chunk size in Terraform core is 8 MB.
23+
ChunkSize int64
24+
}
25+
26+
// ConfigureStateStoreRequest is the framework server request for the
27+
// ConfigureStateStore RPC.
28+
type ConfigureStateStoreRequest struct {
29+
Config *tfsdk.Config
30+
StateStore statestore.StateStore
31+
StateStoreSchema fwschema.Schema
32+
ClientCapabilities ConfigureStateStoreClientCapabilities
33+
}
34+
35+
// ConfigureStateStoreResponse is the framework server response for the
36+
// ConfigureStateStore RPC.
37+
type ConfigureStateStoreResponse struct {
38+
Diagnostics diag.Diagnostics
39+
ServerCapabilities *StateStoreServerCapabilities
40+
}
41+
42+
type StateStoreConfigureData struct {
43+
ServerCapabilities StateStoreServerCapabilities
44+
StateStoreConfigureData any
45+
}
46+
47+
// ConfigureStateStore implements the framework server ConfigureStateStore RPC.
48+
func (s *Server) ConfigureStateStore(ctx context.Context, req *ConfigureStateStoreRequest, resp *ConfigureStateStoreResponse) {
49+
if req == nil {
50+
return
51+
}
52+
53+
nullSchemaData := tftypes.NewValue(req.StateStoreSchema.Type().TerraformType(ctx), nil)
54+
configureReq := statestore.InitializeRequest{
55+
Config: tfsdk.Config{
56+
Schema: req.StateStoreSchema,
57+
Raw: nullSchemaData,
58+
},
59+
ProviderData: s.StateStoreProviderData,
60+
}
61+
if req.Config != nil {
62+
configureReq.Config = *req.Config
63+
}
64+
65+
configureResp := statestore.InitializeResponse{}
66+
67+
logging.FrameworkTrace(ctx, "Calling provider defined StateStore Initialize")
68+
req.StateStore.Initialize(ctx, configureReq, &configureResp)
69+
logging.FrameworkTrace(ctx, "Called provider defined StateStore Initialize")
70+
71+
resp.Diagnostics = configureResp.Diagnostics
72+
resp.ServerCapabilities = &StateStoreServerCapabilities{
73+
ChunkSize: req.ClientCapabilities.ChunkSize,
74+
}
75+
76+
// Set state store configure data + server capabilities for reference in future state store RPCs
77+
s.StateStoreConfigureData = StateStoreConfigureData{
78+
ServerCapabilities: *resp.ServerCapabilities,
79+
StateStoreConfigureData: configureResp.StateStoreData,
80+
}
81+
}

0 commit comments

Comments
 (0)