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 messages
pull/37314/head
Sarah French 8 months ago committed by GitHub
parent 5437cd7c8a
commit 39f7920387
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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)
}
}

@ -347,6 +347,11 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -249,6 +249,11 @@ github.com/tencentyun/cos-go-sdk-v5 v0.7.42 h1:Up1704BJjI5orycXKjpVpvuOInt9GC5pq
github.com/tencentyun/cos-go-sdk-v5 v0.7.42/go.mod h1:LUFnaqRmGk6pEHOaRmdn2dCZR2j0cSsM5xowWFPTPao=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -241,6 +241,11 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -284,6 +284,11 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -245,6 +245,11 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -266,6 +266,11 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -223,6 +223,11 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -289,6 +289,11 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

@ -871,6 +871,16 @@ func (p *MockProvider) ValidateStateStoreConfig(r providers.ValidateStateStoreCo
return resp
}
if p.ValidateStateStoreConfigResponse != nil {
return *p.ValidateStateStoreConfigResponse
}
if p.ValidateStateStoreConfigFn != nil {
return p.ValidateStateStoreConfigFn(r)
}
// In the absence of any custom logic, we do basic validation of the received config against the schema.
//
// Marshall the value to replicate behavior by the GRPC protocol,
// and return any relevant errors
storeSchema, ok := p.getProviderSchema().StateStores[r.TypeName]
@ -879,20 +889,12 @@ func (p *MockProvider) ValidateStateStoreConfig(r providers.ValidateStateStoreCo
return resp
}
if p.ValidateStateStoreConfigResponse != nil {
return *p.ValidateStateStoreConfigResponse
}
_, err := msgpack.Marshal(r.Config, storeSchema.Body.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
if p.ValidateStateStoreConfigFn != nil {
return p.ValidateStateStoreConfigFn(r)
}
return resp
}
@ -908,6 +910,12 @@ func (p *MockProvider) ConfigureStateStore(r providers.ConfigureStateStoreReques
return resp
}
if p.ConfigureStateStoreFn != nil {
return p.ConfigureStateStoreFn(r)
}
// In the absence of any custom logic, we do the logic below.
//
// Marshall the value to replicate behavior by the GRPC protocol,
// and return any relevant errors
storeSchema, ok := p.getProviderSchema().StateStores[r.TypeName]
@ -926,10 +934,6 @@ func (p *MockProvider) ConfigureStateStore(r providers.ConfigureStateStoreReques
return resp
}
if p.ConfigureStateStoreFn != nil {
return p.ConfigureStateStoreFn(r)
}
return resp
}
@ -980,9 +984,6 @@ func (p *MockProvider) DeleteState(r providers.DeleteStateRequest) (resp provide
if !p.ConfigureStateStoreCalled {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("ConfigureStateStore not called before DeleteState %q", r.TypeName))
}
if resp.Diagnostics.HasErrors() {
return resp
}
if p.DeleteStateResponse != nil {
return *p.DeleteStateResponse

@ -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…
Cancel
Save