Skip to content

Commit d6865bf

Browse files
authored
PSS : Implement Lock and Unlock methods on remote grpc client (#37741)
* Implement Lock and Unlock methods on grpcClient * Add tests for Lock and Unlock methods * Reuse same lock info variable
1 parent fc3b1ed commit d6865bf

File tree

2 files changed

+257
-3
lines changed

2 files changed

+257
-3
lines changed

internal/states/remote/remote_grpc.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,26 @@ func (g *grpcClient) Delete() tfdiags.Diagnostics {
110110
// to lock a named state in the remote location.
111111
//
112112
// Implementation of remote.Client
113-
func (g *grpcClient) Lock(*statemgr.LockInfo) (string, error) {
114-
panic("not implemented yet")
113+
func (g *grpcClient) Lock(lock *statemgr.LockInfo) (string, error) {
114+
req := providers.LockStateRequest{
115+
TypeName: g.typeName,
116+
StateId: g.stateId,
117+
Operation: lock.Operation,
118+
}
119+
resp := g.provider.LockState(req)
120+
return resp.LockId, resp.Diagnostics.Err()
115121
}
116122

117123
// Unlock invokes the UnlockState gRPC method in the plugin protocol
118124
// to release a named lock on a specific state in the remote location.
119125
//
120126
// Implementation of remote.Client
121127
func (g *grpcClient) Unlock(id string) error {
122-
panic("not implemented yet")
128+
req := providers.UnlockStateRequest{
129+
TypeName: g.typeName,
130+
StateId: g.stateId,
131+
LockId: id,
132+
}
133+
resp := g.provider.UnlockState(req)
134+
return resp.Diagnostics.Err()
123135
}

internal/states/remote/remote_grpc_test.go

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
1515
"github.com/hashicorp/terraform/internal/states"
1616
"github.com/hashicorp/terraform/internal/states/statefile"
17+
"github.com/hashicorp/terraform/internal/states/statemgr"
1718
"github.com/hashicorp/terraform/internal/tfdiags"
1819
"github.com/zclconf/go-cty/cty"
1920
)
@@ -271,3 +272,244 @@ func Test_grpcClient_Delete(t *testing.T) {
271272
t.Fatal("expected Delete method to call DeleteState method on underlying provider, but it has not been called")
272273
}
273274
}
275+
276+
// Testing grpcClient's Lock method.
277+
// The Lock method on a state manager calls the Lock method of the underlying client.
278+
func Test_grpcClient_Lock(t *testing.T) {
279+
typeName := "foo_bar" // state store 'bar' in provider 'foo'
280+
stateId := "production"
281+
operation := "apply"
282+
lockInfo := &statemgr.LockInfo{
283+
Operation: operation,
284+
// This is sufficient when locking via PSS
285+
}
286+
287+
t.Run("state manager made using grpcClient sends expected values to Lock method", func(t *testing.T) {
288+
expectedLockId := "id-from-mock"
289+
provider := testing_provider.MockProvider{
290+
// Mock a provider and internal state store that
291+
// have both been configured
292+
ConfigureProviderCalled: true,
293+
ConfigureStateStoreCalled: true,
294+
295+
// Check values received by the provider from the Lock method.
296+
LockStateFn: func(req providers.LockStateRequest) providers.LockStateResponse {
297+
if req.TypeName != typeName || req.StateId != stateId || req.Operation != operation {
298+
t.Fatalf("expected provider ReadStateBytes method to receive TypeName %q, StateId %q, and Operation %q, instead got TypeName %q, StateId %q, and Operation %q",
299+
typeName,
300+
stateId,
301+
operation,
302+
req.TypeName,
303+
req.StateId,
304+
req.Operation,
305+
)
306+
}
307+
return providers.LockStateResponse{
308+
LockId: expectedLockId,
309+
}
310+
},
311+
}
312+
313+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
314+
// and invoke the method on that interface that uses Lock.
315+
c := NewRemoteGRPC(&provider, typeName, stateId)
316+
317+
lockId, err := c.Lock(lockInfo)
318+
if err != nil {
319+
t.Fatalf("unexpected error: %s", err)
320+
}
321+
322+
if !provider.LockStateCalled {
323+
t.Fatal("expected remote grpc state manager's Lock method to call Lock method on underlying provider, but it has not been called")
324+
}
325+
if lockId != expectedLockId {
326+
t.Fatalf("unexpected lock id returned, wanted %q, got %q", expectedLockId, lockId)
327+
}
328+
})
329+
330+
t.Run("state manager made using grpcClient returns expected error from Lock method's error diagnostic", func(t *testing.T) {
331+
var diags tfdiags.Diagnostics
332+
diags = diags.Append(&hcl.Diagnostic{
333+
Severity: hcl.DiagError,
334+
Summary: "error forced from test",
335+
Detail: "error forced from test",
336+
})
337+
338+
provider := testing_provider.MockProvider{
339+
// Mock a provider and internal state store that
340+
// have both been configured
341+
ConfigureProviderCalled: true,
342+
ConfigureStateStoreCalled: true,
343+
344+
// Force return of an error.
345+
LockStateResponse: providers.LockStateResponse{
346+
Diagnostics: diags,
347+
},
348+
}
349+
350+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
351+
// and invoke the method on that interface that uses Lock.
352+
c := NewRemoteGRPC(&provider, typeName, stateId)
353+
354+
_, err := c.Lock(lockInfo)
355+
if !provider.LockStateCalled {
356+
t.Fatal("expected remote grpc state manager's Lock method to call Lock method on underlying provider, but it has not been called")
357+
}
358+
if err == nil {
359+
t.Fatal("expected error but got none")
360+
}
361+
expectedMsg := "error forced from test"
362+
if !strings.Contains(err.Error(), expectedMsg) {
363+
t.Fatalf("expected error to include %q but got: %s", expectedMsg, err)
364+
}
365+
})
366+
367+
t.Run("state manager made using grpcClient currently swallows warning diagnostics returned from the Lock method", func(t *testing.T) {
368+
var diags tfdiags.Diagnostics
369+
diags = diags.Append(&hcl.Diagnostic{
370+
Severity: hcl.DiagWarning,
371+
Summary: "warning forced from test",
372+
Detail: "warning forced from test",
373+
})
374+
375+
provider := testing_provider.MockProvider{
376+
// Mock a provider and internal state store that
377+
// have both been configured
378+
ConfigureProviderCalled: true,
379+
ConfigureStateStoreCalled: true,
380+
381+
// Force return of a warning.
382+
LockStateResponse: providers.LockStateResponse{
383+
Diagnostics: diags,
384+
},
385+
}
386+
387+
c := NewRemoteGRPC(&provider, typeName, stateId)
388+
389+
_, err := c.Lock(lockInfo)
390+
if err != nil {
391+
t.Fatalf("unexpected error: %s", err)
392+
}
393+
394+
// The warning is swallowed by the Lock method.
395+
// The Locker interface should be updated to allow use of diagnostics instead of errors,
396+
// and this test should be updated.
397+
})
398+
}
399+
400+
// Testing grpcClient's Unlock method.
401+
// The Unlock method on a state manager calls the Unlock method of the underlying client.
402+
func Test_grpcClient_Unlock(t *testing.T) {
403+
typeName := "foo_bar" // state store 'bar' in provider 'foo'
404+
stateId := "production"
405+
406+
t.Run("state manager made using grpcClient sends expected values to Unlock method", func(t *testing.T) {
407+
expectedLockId := "id-from-mock"
408+
provider := testing_provider.MockProvider{
409+
// Mock a provider and internal state store that
410+
// have both been configured
411+
ConfigureProviderCalled: true,
412+
ConfigureStateStoreCalled: true,
413+
414+
// Check values received by the provider from the Lock method.
415+
UnlockStateFn: func(req providers.UnlockStateRequest) providers.UnlockStateResponse {
416+
if req.TypeName != typeName || req.StateId != stateId || req.LockId != expectedLockId {
417+
t.Fatalf("expected provider ReadStateBytes method to receive TypeName %q, StateId %q, and LockId %q, instead got TypeName %q, StateId %q, and LockId %q",
418+
typeName,
419+
stateId,
420+
expectedLockId,
421+
req.TypeName,
422+
req.StateId,
423+
req.LockId,
424+
)
425+
}
426+
return providers.UnlockStateResponse{}
427+
},
428+
}
429+
430+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
431+
// and invoke the method on that interface that uses Unlock.
432+
c := NewRemoteGRPC(&provider, typeName, stateId)
433+
434+
err := c.Unlock(expectedLockId)
435+
if err != nil {
436+
t.Fatalf("unexpected error: %s", err)
437+
}
438+
if !provider.UnlockStateCalled {
439+
t.Fatal("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called")
440+
}
441+
442+
})
443+
444+
t.Run("state manager made using grpcClient returns expected error from Unlock method's error diagnostic", func(t *testing.T) {
445+
var diags tfdiags.Diagnostics
446+
diags = diags.Append(&hcl.Diagnostic{
447+
Severity: hcl.DiagError,
448+
Summary: "error forced from test",
449+
Detail: "error forced from test",
450+
})
451+
452+
provider := testing_provider.MockProvider{
453+
// Mock a provider and internal state store that
454+
// have both been configured
455+
ConfigureProviderCalled: true,
456+
ConfigureStateStoreCalled: true,
457+
458+
// Force return of an error.
459+
UnlockStateResponse: providers.UnlockStateResponse{
460+
Diagnostics: diags,
461+
},
462+
}
463+
464+
// This package will be consumed in a statemgr.Full, so we test using NewRemoteGRPC
465+
// and invoke the method on that interface that uses Unlock.
466+
c := NewRemoteGRPC(&provider, typeName, stateId)
467+
468+
err := c.Unlock("foobar") // argument used here isn't important in this test
469+
if !provider.UnlockStateCalled {
470+
t.Fatal("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called")
471+
}
472+
if err == nil {
473+
t.Fatal("expected error but got none")
474+
}
475+
expectedMsg := "error forced from test"
476+
if !strings.Contains(err.Error(), expectedMsg) {
477+
t.Fatalf("expected error to include %q but got: %s", expectedMsg, err)
478+
}
479+
})
480+
481+
t.Run("state manager made using grpcClient currently swallows warning diagnostics returned from the Unlock method", func(t *testing.T) {
482+
var diags tfdiags.Diagnostics
483+
diags = diags.Append(&hcl.Diagnostic{
484+
Severity: hcl.DiagWarning,
485+
Summary: "warning forced from test",
486+
Detail: "warning forced from test",
487+
})
488+
489+
provider := testing_provider.MockProvider{
490+
// Mock a provider and internal state store that
491+
// have both been configured
492+
ConfigureProviderCalled: true,
493+
ConfigureStateStoreCalled: true,
494+
495+
// Force return of a warning.
496+
UnlockStateResponse: providers.UnlockStateResponse{
497+
Diagnostics: diags,
498+
},
499+
}
500+
501+
c := NewRemoteGRPC(&provider, typeName, stateId)
502+
503+
err := c.Unlock("foobar") // argument used here isn't important in this test
504+
if !provider.UnlockStateCalled {
505+
t.Fatal("expected remote grpc state manager's Unlock method to call Unlock method on underlying provider, but it has not been called")
506+
}
507+
if err != nil {
508+
t.Fatalf("unexpected error: %s", err)
509+
}
510+
511+
// The warning is swallowed by the Unlock method.
512+
// The Locker interface should be updated to allow use of diagnostics instead of errors,
513+
// and this test should be updated.
514+
})
515+
}

0 commit comments

Comments
 (0)