mirror of https://github.com/hashicorp/terraform
PSS: Add packages for presenting a provider as an instance of backend.Backend (#37291)
* Add code to 'wrap' a provider implementation so that it can behave like a backend.Backend * Add code for creating a state manager that will interact with a provider via grpc to interact with state * Remove prototyping code * Update old implementation of PrepareConfig on Pluggable to match new RPC names * Implement Configure method on Pluggable * Implement Workspaces and DeleteWorkspace methods on Pluggable * Prevent construction of a pluggable with missing data, add godoc comment to NewPluggable * Add godoc comment to ConfigSchema * Refactor how we create a state manager for interacting with PSS Now we reuse the remote.State struct and all the pre-existing logic there. We still wrap the provider interface to enable use of gRPC methods, but that's now done using the remote.Client interface. * Rename file * Move file into the remote package * Rename file and package to `pluggable` * Add test for the only method implemented fully on `grpcClient` : Delete * Add tests for `NewPluggable` * Add tests for (Pluggable).ConfigSchema method, make minor fixes * Change mocks: hardcoded response or logic should return before default logic in method. * Add tests for (Pluggable).PrepareConfig method * Add tests for (Pluggable).Configure method * Add tests for (Pluggable).Workspaces method * Add tests for (Pluggable).DeleteWorkspace method * Fix rebase * Run `make syncdeps` * Add headers * Add missing comments * No need to implement ClientForcePusher * Apply feedback from review, make small tweaks to test failure messagespull/37314/head
parent
5437cd7c8a
commit
39f7920387
@ -0,0 +1,140 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package pluggable
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/backend"
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states/remote"
|
||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// NewPluggable returns an instance of the backend.Backend interface that
|
||||
// contains a provider interface. These are the assumptions about that
|
||||
// provider:
|
||||
//
|
||||
// * The provider implements at least one state store.
|
||||
// * The provider has already been configured before using NewPluggable.
|
||||
//
|
||||
// The state store could also be configured prior to using NewPluggable,
|
||||
// or it could be configured using the relevant backend.Backend methods.
|
||||
//
|
||||
// By wrapping a configured provider in a Pluggable we allow calling code
|
||||
// to use the provider's gRPC methods when interacting with state.
|
||||
func NewPluggable(p providers.Interface, typeName string) (backend.Backend, error) {
|
||||
if p == nil {
|
||||
return nil, errors.New("Attempted to initialize pluggable state with a nil provider interface. This is a bug in Terraform and should be reported")
|
||||
}
|
||||
if typeName == "" {
|
||||
return nil, errors.New("Attempted to initialize pluggable state with an empty string identifier for the state store. This is a bug in Terraform and should be reported")
|
||||
}
|
||||
|
||||
return &Pluggable{
|
||||
provider: p,
|
||||
typeName: typeName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ backend.Backend = &Pluggable{}
|
||||
|
||||
type Pluggable struct {
|
||||
provider providers.Interface
|
||||
typeName string
|
||||
}
|
||||
|
||||
// ConfigSchema returns the schema for the state store implementation
|
||||
// name provided when the Pluggable was constructed.
|
||||
//
|
||||
// ConfigSchema implements backend.Backend
|
||||
func (p *Pluggable) ConfigSchema() *configschema.Block {
|
||||
schemaResp := p.provider.GetProviderSchema()
|
||||
if len(schemaResp.StateStores) == 0 {
|
||||
// No state stores
|
||||
return nil
|
||||
}
|
||||
val, ok := schemaResp.StateStores[p.typeName]
|
||||
if !ok {
|
||||
// Cannot find state store with that type
|
||||
return nil
|
||||
}
|
||||
|
||||
// State store type exists
|
||||
return val.Body
|
||||
}
|
||||
|
||||
// PrepareConfig validates configuration for the state store in
|
||||
// the state storage provider. The configuration sent from Terraform core
|
||||
// will not include any values from environment variables; it is the
|
||||
// provider's responsibility to access any environment variables
|
||||
// to get the complete set of configuration prior to validating it.
|
||||
//
|
||||
// PrepareConfig implements backend.Backend
|
||||
func (p *Pluggable) PrepareConfig(config cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
req := providers.ValidateStateStoreConfigRequest{
|
||||
TypeName: p.typeName,
|
||||
Config: config,
|
||||
}
|
||||
resp := p.provider.ValidateStateStoreConfig(req)
|
||||
return config, resp.Diagnostics
|
||||
}
|
||||
|
||||
// Configure configures the state store in the state storage provider.
|
||||
// Calling code is expected to have already validated the config using
|
||||
// the PrepareConfig method.
|
||||
//
|
||||
// It is the provider's responsibility to access any environment variables
|
||||
// set by the user to get the complete set of configuration.
|
||||
//
|
||||
// Configure implements backend.Backend
|
||||
func (p *Pluggable) Configure(config cty.Value) tfdiags.Diagnostics {
|
||||
req := providers.ConfigureStateStoreRequest{
|
||||
TypeName: p.typeName,
|
||||
Config: config,
|
||||
}
|
||||
resp := p.provider.ConfigureStateStore(req)
|
||||
return resp.Diagnostics
|
||||
}
|
||||
|
||||
// Workspaces returns a list of all states/CE workspaces that the backend.Backend
|
||||
// can find, given how it is configured. For example returning a list of differently
|
||||
// -named files in a blob storage service.
|
||||
//
|
||||
// Workspace implements backend.Backend
|
||||
func (p *Pluggable) Workspaces() ([]string, error) {
|
||||
req := providers.GetStatesRequest{
|
||||
TypeName: p.typeName,
|
||||
}
|
||||
resp := p.provider.GetStates(req)
|
||||
|
||||
return resp.States, resp.Diagnostics.Err()
|
||||
}
|
||||
|
||||
// DeleteWorkspace deletes the state file for the named workspace.
|
||||
// The state storage provider is expected to return error diagnostics
|
||||
// if the workspace doesn't exist or it is unable to be deleted.
|
||||
//
|
||||
// DeleteWorkspace implements backend.Backend
|
||||
func (p *Pluggable) DeleteWorkspace(workspace string, force bool) error {
|
||||
req := providers.DeleteStateRequest{
|
||||
TypeName: p.typeName,
|
||||
StateId: workspace,
|
||||
}
|
||||
resp := p.provider.DeleteState(req)
|
||||
return resp.Diagnostics.Err()
|
||||
}
|
||||
|
||||
// StateMgr returns a state manager that uses gRPC to communicate with the
|
||||
// state storage provider to interact with state.
|
||||
//
|
||||
// StateMgr implements backend.Backend
|
||||
func (p *Pluggable) StateMgr(workspace string) (statemgr.Full, error) {
|
||||
// repackages the provider's methods inside a state manager,
|
||||
// to be passed to the calling code that expects a statemgr.Full
|
||||
return remote.NewRemoteGRPC(p.provider, p.typeName, workspace), nil
|
||||
}
|
||||
@ -0,0 +1,400 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package pluggable
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/configs/configschema"
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestNewPluggable(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
provider providers.Interface
|
||||
typeName string
|
||||
|
||||
wantError string
|
||||
}{
|
||||
"no error when inputs are provided": {
|
||||
provider: &testing_provider.MockProvider{},
|
||||
typeName: "foo_bar",
|
||||
},
|
||||
"no error when store name has underscores": {
|
||||
provider: &testing_provider.MockProvider{},
|
||||
// foo provider containing fizz_buzz store
|
||||
typeName: "foo_fizz_buzz",
|
||||
},
|
||||
"error when store type not provided": {
|
||||
provider: &testing_provider.MockProvider{},
|
||||
typeName: "",
|
||||
wantError: "Attempted to initialize pluggable state with an empty string identifier for the state store.",
|
||||
},
|
||||
"error when provider interface is nil": {
|
||||
provider: nil,
|
||||
typeName: "foo_bar",
|
||||
wantError: "Attempted to initialize pluggable state with a nil provider interface.",
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
_, err := NewPluggable(tc.provider, tc.typeName)
|
||||
if err != nil {
|
||||
if tc.wantError == "" {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantError) {
|
||||
t.Fatalf("expected error %q but got %q", tc.wantError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil && tc.wantError != "" {
|
||||
t.Fatalf("expected error %q but got none", tc.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluggable_ConfigSchema(t *testing.T) {
|
||||
|
||||
p := &testing_provider.MockProvider{
|
||||
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
|
||||
Provider: providers.Schema{},
|
||||
DataSources: map[string]providers.Schema{},
|
||||
ResourceTypes: map[string]providers.Schema{},
|
||||
ListResourceTypes: map[string]providers.Schema{},
|
||||
StateStores: map[string]providers.Schema{
|
||||
// This imagines a provider called foo that contains
|
||||
// two pluggable state store implementations, called
|
||||
// bar and baz.
|
||||
// It's accurate to include the prefixed provider name
|
||||
// in the keys of schema maps
|
||||
"foo_bar": {
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"bar": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"foo_baz": {
|
||||
Body: &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"baz": {
|
||||
Type: cty.String,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
provider providers.Interface
|
||||
typeName string
|
||||
|
||||
expectedAttrName string
|
||||
expectNil bool
|
||||
}{
|
||||
"returns expected schema - bar store": {
|
||||
provider: p,
|
||||
typeName: "foo_bar",
|
||||
expectedAttrName: "bar",
|
||||
},
|
||||
"returns expected schema - baz store": {
|
||||
provider: p,
|
||||
typeName: "foo_baz",
|
||||
expectedAttrName: "baz",
|
||||
},
|
||||
"returns nil if there isn't a store with a matching name": {
|
||||
provider: p,
|
||||
typeName: "foo_not_implemented",
|
||||
expectNil: true,
|
||||
},
|
||||
"returns nil if no state stores are implemented in the provider": {
|
||||
provider: &testing_provider.MockProvider{},
|
||||
typeName: "foo_bar",
|
||||
expectNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
p, err := NewPluggable(tc.provider, tc.typeName)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
s := p.ConfigSchema()
|
||||
if mock, ok := tc.provider.(*testing_provider.MockProvider); ok {
|
||||
if !mock.GetProviderSchemaCalled {
|
||||
t.Fatal("expected mock's GetProviderSchema method to have been called")
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
if !tc.expectNil {
|
||||
t.Fatal("ConfigSchema returned an unexpected nil schema")
|
||||
}
|
||||
return
|
||||
}
|
||||
if val := s.Attributes[tc.expectedAttrName]; val == nil {
|
||||
t.Fatalf("expected the returned schema to include an attr called %q, but it was missing. Schema contains attrs: %v",
|
||||
tc.expectedAttrName,
|
||||
slices.Sorted(maps.Keys(s.Attributes)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluggable_PrepareConfig(t *testing.T) {
|
||||
fooBar := "foo_bar"
|
||||
cases := map[string]struct {
|
||||
provider providers.Interface
|
||||
typeName string
|
||||
config cty.Value
|
||||
|
||||
wantError string
|
||||
}{
|
||||
"when config is deemed valid there are no diagnostics": {
|
||||
provider: &testing_provider.MockProvider{
|
||||
ConfigureProviderCalled: true,
|
||||
ValidateStateStoreConfigFn: func(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
// if validation is ok, response has no diags
|
||||
return providers.ValidateStateStoreConfigResponse{}
|
||||
},
|
||||
},
|
||||
typeName: fooBar,
|
||||
},
|
||||
"errors are returned, and expected arguments are in the request": {
|
||||
provider: &testing_provider.MockProvider{
|
||||
ConfigureProviderCalled: true,
|
||||
ValidateStateStoreConfigFn: func(req providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
|
||||
// Are the right values being put into the incoming request?
|
||||
if req.TypeName != fooBar || req.Config != cty.True {
|
||||
t.Fatalf("expected provider ValidateStateStoreConfig method to receive TypeName %q and Config %q, instead got TypeName %q and Config %q",
|
||||
fooBar,
|
||||
cty.True,
|
||||
req.TypeName,
|
||||
req.Config)
|
||||
}
|
||||
|
||||
// Force an error, to see it makes it back to the invoked method ok
|
||||
resp := providers.ValidateStateStoreConfigResponse{}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("error diagnostic raised from mock"))
|
||||
return resp
|
||||
},
|
||||
},
|
||||
typeName: fooBar,
|
||||
config: cty.BoolVal(true),
|
||||
wantError: "error diagnostic raised from mock",
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
p, err := NewPluggable(tc.provider, tc.typeName)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
_, diags := p.PrepareConfig(tc.config)
|
||||
if mock, ok := tc.provider.(*testing_provider.MockProvider); ok {
|
||||
if !mock.ValidateStateStoreConfigCalled {
|
||||
t.Fatal("expected mock's ValidateStateStoreConfig method to have been called")
|
||||
}
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
if tc.wantError == "" {
|
||||
t.Fatalf("unexpected error: %s", diags.Err())
|
||||
}
|
||||
if !strings.Contains(diags.Err().Error(), tc.wantError) {
|
||||
t.Fatalf("expected error %q but got: %q", tc.wantError, diags.Err())
|
||||
}
|
||||
return
|
||||
}
|
||||
if !diags.HasErrors() && tc.wantError != "" {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluggable_Configure(t *testing.T) {
|
||||
|
||||
// Arrange mocks
|
||||
typeName := "foo_bar"
|
||||
wantError := "error diagnostic raised from mock"
|
||||
mock := &testing_provider.MockProvider{
|
||||
ConfigureProviderCalled: true,
|
||||
ValidateStateStoreConfigCalled: true,
|
||||
ConfigureStateStoreFn: func(req providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
|
||||
if req.TypeName != typeName || req.Config != cty.True {
|
||||
t.Fatalf("expected provider ConfigureStateStore method to receive TypeName %q and Config %q, instead got TypeName %q and Config %q",
|
||||
typeName,
|
||||
cty.True,
|
||||
req.TypeName,
|
||||
req.Config)
|
||||
}
|
||||
|
||||
resp := providers.ConfigureStateStoreResponse{}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New(wantError))
|
||||
return resp
|
||||
},
|
||||
}
|
||||
|
||||
// Make Pluggable and invoke Configure
|
||||
p, err := NewPluggable(mock, typeName)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// This isn't representative of true values used with the method, but is sufficient
|
||||
// for testing that the mock receives the provided value as expected.
|
||||
config := cty.BoolVal(true)
|
||||
diags := p.Configure(config)
|
||||
|
||||
// Assertions
|
||||
if !mock.ValidateStateStoreConfigCalled {
|
||||
t.Fatal("expected mock's ValidateStateStoreConfig method to have been called")
|
||||
}
|
||||
if !diags.HasErrors() {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
if !strings.Contains(diags.Err().Error(), wantError) {
|
||||
t.Fatalf("expected error %q but got: %q", wantError, diags.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluggable_Workspaces(t *testing.T) {
|
||||
fooBar := "foo_bar"
|
||||
cases := map[string]struct {
|
||||
provider providers.Interface
|
||||
expectedWorkspaces []string
|
||||
wantError string
|
||||
}{
|
||||
"returned workspaces match what's returned from the store": {
|
||||
// and "default" isn't included by default
|
||||
provider: &testing_provider.MockProvider{
|
||||
ConfigureProviderCalled: true,
|
||||
ValidateStateStoreConfigCalled: true,
|
||||
ConfigureStateStoreCalled: true,
|
||||
GetStatesFn: func(req providers.GetStatesRequest) providers.GetStatesResponse {
|
||||
workspaces := []string{"abcd", "efg"}
|
||||
resp := providers.GetStatesResponse{
|
||||
States: workspaces,
|
||||
}
|
||||
return resp
|
||||
},
|
||||
},
|
||||
expectedWorkspaces: []string{"abcd", "efg"},
|
||||
},
|
||||
"errors are returned, and expected arguments are in the request": {
|
||||
provider: &testing_provider.MockProvider{
|
||||
ConfigureProviderCalled: true,
|
||||
ValidateStateStoreConfigCalled: true,
|
||||
ConfigureStateStoreCalled: true,
|
||||
GetStatesFn: func(req providers.GetStatesRequest) providers.GetStatesResponse {
|
||||
if req.TypeName != fooBar {
|
||||
t.Fatalf("expected provider GetStates method to receive TypeName %q, instead got TypeName %q",
|
||||
fooBar,
|
||||
req.TypeName)
|
||||
}
|
||||
resp := providers.GetStatesResponse{}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("error diagnostic raised from mock"))
|
||||
return resp
|
||||
},
|
||||
},
|
||||
wantError: "error diagnostic raised from mock",
|
||||
},
|
||||
}
|
||||
|
||||
for tn, tc := range cases {
|
||||
t.Run(tn, func(t *testing.T) {
|
||||
p, err := NewPluggable(tc.provider, fooBar)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
workspaces, err := p.Workspaces()
|
||||
if mock, ok := tc.provider.(*testing_provider.MockProvider); ok {
|
||||
if !mock.GetStatesCalled {
|
||||
t.Fatal("expected mock's GetStates method to have been called")
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if tc.wantError == "" {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantError) {
|
||||
t.Fatalf("expected error %q but got: %q", tc.wantError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tc.wantError != "" {
|
||||
t.Fatal("expected an error but got none")
|
||||
}
|
||||
|
||||
if slices.Compare(workspaces, tc.expectedWorkspaces) != 0 {
|
||||
t.Fatalf("expected workspaces %v, got %v", tc.expectedWorkspaces, workspaces)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluggable_DeleteWorkspace(t *testing.T) {
|
||||
|
||||
// Arrange mocks
|
||||
typeName := "foo_bar"
|
||||
stateId := "my-state"
|
||||
mock := &testing_provider.MockProvider{
|
||||
ConfigureProviderCalled: true,
|
||||
ValidateStateStoreConfigCalled: true,
|
||||
ConfigureStateStoreCalled: true,
|
||||
DeleteStateFn: func(req providers.DeleteStateRequest) providers.DeleteStateResponse {
|
||||
if req.TypeName != typeName || req.StateId != stateId {
|
||||
t.Fatalf("expected provider DeleteState method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q",
|
||||
typeName,
|
||||
stateId,
|
||||
req.TypeName,
|
||||
req.StateId,
|
||||
)
|
||||
}
|
||||
resp := providers.DeleteStateResponse{}
|
||||
resp.Diagnostics = resp.Diagnostics.Append(errors.New("error diagnostic raised from mock"))
|
||||
return resp
|
||||
},
|
||||
}
|
||||
|
||||
// Make Pluggable and invoke DeleteWorkspace
|
||||
p, err := NewPluggable(mock, typeName)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
err = p.DeleteWorkspace(stateId, false)
|
||||
|
||||
// Assertions
|
||||
if !mock.DeleteStateCalled {
|
||||
t.Fatal("expected mock's DeleteState method to have been called")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("test is expected to return an error, but there isn't one")
|
||||
}
|
||||
wantError := "error diagnostic raised from mock"
|
||||
if !strings.Contains(err.Error(), wantError) {
|
||||
t.Fatalf("expected error %q but got: %q", wantError, err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package remote
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
"github.com/hashicorp/terraform/internal/states/statemgr"
|
||||
)
|
||||
|
||||
// NewRemoteGRPC returns a remote state manager (remote.State) containing
|
||||
// an implementation of remote.Client that allows Terraform to interact with
|
||||
// a provider implementing pluggable state storage.
|
||||
//
|
||||
// The remote.Client implementation's methods invoke the provider's RPC
|
||||
// methods to perform tasks like reading in state, locking, etc.
|
||||
//
|
||||
// NewRemoteGRPC requires these arguments to create the remote.Client:
|
||||
// 1) the provider interface, needed to call gRPC methods
|
||||
// 2) the name of the state storage implementation in the provider
|
||||
// 3) the name of the state/the active workspace
|
||||
func NewRemoteGRPC(provider providers.Interface, typeName string, stateId string) statemgr.Full {
|
||||
mgr := &State{
|
||||
Client: &grpcClient{
|
||||
provider: provider,
|
||||
typeName: typeName,
|
||||
stateId: stateId,
|
||||
},
|
||||
}
|
||||
return mgr
|
||||
}
|
||||
|
||||
var (
|
||||
_ Client = &grpcClient{}
|
||||
_ ClientLocker = &grpcClient{}
|
||||
)
|
||||
|
||||
// grpcClient acts like a client to enable the State state manager
|
||||
// to communicate with a provider that implements pluggable state
|
||||
// storage via gRPC.
|
||||
//
|
||||
// The calling code needs to provide information about the store's name
|
||||
// and the name of the state (i.e. CE workspace) to use, as these are
|
||||
// arguments required in gRPC requests.
|
||||
type grpcClient struct {
|
||||
provider providers.Interface
|
||||
typeName string // the state storage implementation's name
|
||||
stateId string
|
||||
}
|
||||
|
||||
// Get invokes the ReadStateBytes gRPC method in the plugin protocol
|
||||
// and returns a copy of the downloaded state data.
|
||||
//
|
||||
// Implementation of remote.Client
|
||||
func (g *grpcClient) Get() (*Payload, error) {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
// Put invokes the WriteStateBytes gRPC method in the plugin protocol
|
||||
// and to transfer state data to the remote location.
|
||||
//
|
||||
// Implementation of remote.Client
|
||||
func (g *grpcClient) Put(state []byte) error {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
// Delete invokes the DeleteState gRPC method in the plugin protocol
|
||||
// to delete a named state in the remote location.
|
||||
//
|
||||
// NOTE: this is included to fulfil an interface, but deletion of
|
||||
// workspaces is actually achieved through the backend.Backend
|
||||
// interface's DeleteWorkspace method.
|
||||
//
|
||||
// Implementation of remote.Client
|
||||
func (g *grpcClient) Delete() error {
|
||||
req := providers.DeleteStateRequest{
|
||||
TypeName: g.typeName,
|
||||
StateId: g.stateId,
|
||||
}
|
||||
resp := g.provider.DeleteState(req)
|
||||
return resp.Diagnostics.Err()
|
||||
}
|
||||
|
||||
// Lock invokes the LockState gRPC method in the plugin protocol
|
||||
// to lock a named state in the remote location.
|
||||
//
|
||||
// Implementation of remote.Client
|
||||
func (g *grpcClient) Lock(*statemgr.LockInfo) (string, error) {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
// Unlock invokes the UnlockState gRPC method in the plugin protocol
|
||||
// to release a named lock on a specific state in the remote location.
|
||||
//
|
||||
// Implementation of remote.Client
|
||||
func (g *grpcClient) Unlock(id string) error {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package remote
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/providers"
|
||||
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
|
||||
)
|
||||
|
||||
// Testing grpcClient's Delete method.
|
||||
// This method is needed to implement the remote.Client interface, but
|
||||
// this is not invoked by the remote state manager (remote.State) that
|
||||
// wil contain the client.
|
||||
//
|
||||
// In future we should remove the need for a Delete method in
|
||||
// remote.Client, but for now it is implemented and tested.
|
||||
func Test_grpcClient_Delete(t *testing.T) {
|
||||
typeName := "foo_bar" // state store 'bar' in provider 'foo'
|
||||
stateId := "production"
|
||||
|
||||
provider := testing_provider.MockProvider{
|
||||
// Mock a provider and internal state store that
|
||||
// have both been configured
|
||||
ConfigureProviderCalled: true,
|
||||
ConfigureStateStoreCalled: true,
|
||||
|
||||
// Check values received by the provider from the Delete method.
|
||||
DeleteStateFn: func(req providers.DeleteStateRequest) providers.DeleteStateResponse {
|
||||
if req.TypeName != typeName || req.StateId != stateId {
|
||||
t.Fatalf("expected provider DeleteState method to receive TypeName %q and StateId %q, instead got TypeName %q and StateId %q",
|
||||
typeName,
|
||||
stateId,
|
||||
req.TypeName,
|
||||
req.StateId)
|
||||
}
|
||||
return providers.DeleteStateResponse{
|
||||
// no diags
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Delete isn't accessible via a statemgr.Full, so we don't use NewRemoteGRPC.
|
||||
// See comment above test for more information.
|
||||
c := grpcClient{
|
||||
provider: &provider,
|
||||
typeName: typeName,
|
||||
stateId: stateId,
|
||||
}
|
||||
|
||||
err := c.Delete()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if !provider.DeleteStateCalled {
|
||||
t.Fatal("expected Delete method to call DeleteState method on underlying provider, but it has not been called")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue