Skip to content

Commit aed61af

Browse files
authored
PSS: Implement Get and Put methods on remote grpc state, using Read/WriteStateBytes RPCs (#37717)
* Implement Get method on remote gRPC state client, add tests * Implement Put method on remote gRPC state client, add tests
1 parent 312f296 commit aed61af

File tree

2 files changed

+239
-3
lines changed

2 files changed

+239
-3
lines changed

internal/states/remote/remote_grpc.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,39 @@ type grpcClient struct {
5454
//
5555
// Implementation of remote.Client
5656
func (g *grpcClient) Get() (*Payload, tfdiags.Diagnostics) {
57-
panic("not implemented yet")
57+
req := providers.ReadStateBytesRequest{
58+
TypeName: g.typeName,
59+
StateId: g.stateId,
60+
}
61+
resp := g.provider.ReadStateBytes(req)
62+
63+
if len(resp.Bytes) == 0 {
64+
// No state to return
65+
return nil, resp.Diagnostics
66+
}
67+
68+
// TODO: Remove or replace use of MD5?
69+
// The MD5 value here is never used.
70+
payload := &Payload{
71+
Data: resp.Bytes,
72+
MD5: []byte{}, // empty, as this is unused downstream
73+
}
74+
return payload, resp.Diagnostics
5875
}
5976

6077
// Put invokes the WriteStateBytes gRPC method in the plugin protocol
6178
// and to transfer state data to the remote location.
6279
//
6380
// Implementation of remote.Client
6481
func (g *grpcClient) Put(state []byte) tfdiags.Diagnostics {
65-
panic("not implemented yet")
82+
req := providers.WriteStateBytesRequest{
83+
TypeName: g.typeName,
84+
StateId: g.stateId,
85+
Bytes: state,
86+
}
87+
resp := g.provider.WriteStateBytes(req)
88+
89+
return resp.Diagnostics
6690
}
6791

6892
// Delete invokes the DeleteState gRPC method in the plugin protocol

internal/states/remote/remote_grpc_test.go

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,228 @@
44
package remote
55

66
import (
7+
"bytes"
8+
"strings"
79
"testing"
810

11+
"github.com/hashicorp/hcl/v2"
12+
"github.com/hashicorp/terraform/internal/addrs"
913
"github.com/hashicorp/terraform/internal/providers"
1014
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
15+
"github.com/hashicorp/terraform/internal/states"
16+
"github.com/hashicorp/terraform/internal/states/statefile"
17+
"github.com/hashicorp/terraform/internal/tfdiags"
18+
"github.com/zclconf/go-cty/cty"
1119
)
1220

21+
// Testing grpcClient's Get method, via the state manager made using a grpcClient.
22+
// The RefreshState method on a state manager calls the Get method of the underlying client.
23+
func Test_grpcClient_Get(t *testing.T) {
24+
typeName := "foo_bar" // state store 'bar' in provider 'foo'
25+
stateId := "production"
26+
stateString := `{
27+
"version": 4,
28+
"terraform_version": "0.13.0",
29+
"serial": 0,
30+
"lineage": "",
31+
"outputs": {
32+
"foo": {
33+
"value": "bar",
34+
"type": "string"
35+
}
36+
}
37+
}`
38+
39+
t.Run("state manager made using grpcClient returns expected state", func(t *testing.T) {
40+
provider := testing_provider.MockProvider{
41+
// Mock a provider and internal state store that
42+
// have both been configured
43+
ConfigureProviderCalled: true,
44+
ConfigureStateStoreCalled: true,
45+
46+
// Check values received by the provider from the Get method.
47+
ReadStateBytesFn: func(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse {
48+
if req.TypeName != typeName || req.StateId != stateId {
49+
t.Fatalf("expected provider ReadStateBytes method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q",
50+
typeName,
51+
stateId,
52+
req.TypeName,
53+
req.StateId)
54+
}
55+
return providers.ReadStateBytesResponse{
56+
Bytes: []byte(stateString),
57+
// no diags
58+
}
59+
},
60+
}
61+
62+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
63+
// and invoke the method on that interface that uses Get.
64+
c := NewRemoteGRPC(&provider, typeName, stateId)
65+
66+
err := c.RefreshState() // Calls Get
67+
if err != nil {
68+
t.Fatalf("unexpected error: %s", err)
69+
}
70+
71+
if !provider.ReadStateBytesCalled {
72+
t.Fatal("expected remote grpc state manager's RefreshState method to, via Get, call ReadStateBytes method on underlying provider, but it has not been called")
73+
}
74+
s := c.State()
75+
v, ok := s.RootOutputValues["foo"]
76+
if !ok {
77+
t.Fatal("state manager doesn't contain the state returned by the mock")
78+
}
79+
if v.Value.AsString() != "bar" {
80+
t.Fatal("state manager doesn't contain the correct output value in the state")
81+
}
82+
})
83+
84+
t.Run("state manager made using grpcClient returns expected error from error diagnostic", func(t *testing.T) {
85+
var diags tfdiags.Diagnostics
86+
diags = diags.Append(&hcl.Diagnostic{
87+
Severity: hcl.DiagError,
88+
Summary: "error forced from test",
89+
Detail: "error forced from test",
90+
})
91+
provider := testing_provider.MockProvider{
92+
// Mock a provider and internal state store that
93+
// have both been configured
94+
ConfigureProviderCalled: true,
95+
ConfigureStateStoreCalled: true,
96+
97+
// Force an error diagnostic
98+
ReadStateBytesFn: func(req providers.ReadStateBytesRequest) providers.ReadStateBytesResponse {
99+
return providers.ReadStateBytesResponse{
100+
// we don't expect state to accompany an error, but this test shows that
101+
// if an error us present amy state returned is ignored.
102+
Bytes: []byte(stateString),
103+
Diagnostics: diags,
104+
}
105+
},
106+
}
107+
108+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
109+
// and invoke the method on that interface that uses Get.
110+
c := NewRemoteGRPC(&provider, typeName, stateId)
111+
112+
err := c.RefreshState() // Calls Get
113+
if err == nil {
114+
t.Fatal("expected an error but got none")
115+
}
116+
117+
if !provider.ReadStateBytesCalled {
118+
t.Fatal("expected remote grpc state manager's RefreshState method to, via Get, call ReadStateBytes method on underlying provider, but it has not been called")
119+
}
120+
s := c.State()
121+
if s != nil {
122+
t.Fatalf("expected refresh to fail due to error diagnostic, but state has been refreshed: %s", s.String())
123+
}
124+
})
125+
}
126+
127+
// Testing grpcClient's Put method, via the state manager made using a grpcClient.
128+
// The PersistState method on a state manager calls the Put method of the underlying client.
129+
func Test_grpcClient_Put(t *testing.T) {
130+
typeName := "foo_bar" // state store 'bar' in provider 'foo'
131+
stateId := "production"
132+
133+
// State with 1 output
134+
s := states.NewState()
135+
s.SetOutputValue(addrs.AbsOutputValue{
136+
Module: addrs.RootModuleInstance,
137+
OutputValue: addrs.OutputValue{Name: "foo"},
138+
}, cty.StringVal("bar"), false)
139+
140+
t.Run("state manager made using grpcClient writes the expected state", func(t *testing.T) {
141+
provider := testing_provider.MockProvider{
142+
// Mock a provider and internal state store that
143+
// have both been configured
144+
ConfigureProviderCalled: true,
145+
ConfigureStateStoreCalled: true,
146+
147+
// Check values received by the provider from the Put method.
148+
WriteStateBytesFn: func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse {
149+
if req.TypeName != typeName || req.StateId != stateId {
150+
t.Fatalf("expected provider WriteStateBytes method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q",
151+
typeName,
152+
stateId,
153+
req.TypeName,
154+
req.StateId)
155+
}
156+
157+
r := bytes.NewReader(req.Bytes)
158+
reqState, err := statefile.Read(r)
159+
if err != nil {
160+
t.Fatal(err)
161+
}
162+
if reqState.State.String() != s.String() {
163+
t.Fatalf("wanted state %s got %s", s.String(), reqState.State.String())
164+
}
165+
return providers.WriteStateBytesResponse{
166+
// no diags
167+
}
168+
},
169+
}
170+
171+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
172+
// and invoke the method on that interface that uses Put.
173+
c := NewRemoteGRPC(&provider, typeName, stateId)
174+
175+
// Set internal state value that will be persisted.
176+
c.WriteState(s)
177+
178+
// Test PersistState, which uses Put.
179+
err := c.PersistState(nil)
180+
if err != nil {
181+
t.Fatalf("unexpected error: %s", err)
182+
}
183+
})
184+
185+
t.Run("state manager made using grpcClient returns expected error from error diagnostic", func(t *testing.T) {
186+
expectedErr := "error forced from test"
187+
var diags tfdiags.Diagnostics
188+
diags = diags.Append(&hcl.Diagnostic{
189+
Severity: hcl.DiagError,
190+
Summary: expectedErr,
191+
Detail: expectedErr,
192+
})
193+
provider := testing_provider.MockProvider{
194+
// Mock a provider and internal state store that
195+
// have both been configured
196+
ConfigureProviderCalled: true,
197+
ConfigureStateStoreCalled: true,
198+
199+
// Force an error diagnostic
200+
WriteStateBytesFn: func(req providers.WriteStateBytesRequest) providers.WriteStateBytesResponse {
201+
return providers.WriteStateBytesResponse{
202+
Diagnostics: diags,
203+
}
204+
},
205+
}
206+
207+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
208+
// and invoke the method on that interface that uses Get.
209+
c := NewRemoteGRPC(&provider, typeName, stateId)
210+
211+
// Set internal state value that will be persisted.
212+
c.WriteState(s)
213+
214+
// Test PersistState, which uses Put.
215+
err := c.PersistState(nil)
216+
if err == nil {
217+
t.Fatalf("expected error but got none")
218+
}
219+
if !strings.Contains(err.Error(), expectedErr) {
220+
t.Fatalf("expected error to contain %q, but got: %s", expectedErr, err.Error())
221+
}
222+
})
223+
}
224+
13225
// Testing grpcClient's Delete method.
14226
// This method is needed to implement the remote.Client interface, but
15227
// this is not invoked by the remote state manager (remote.State) that
16-
// wil contain the client.
228+
// will contain the client.
17229
//
18230
// In future we should remove the need for a Delete method in
19231
// remote.Client, but for now it is implemented and tested.

0 commit comments

Comments
 (0)