PSS: Implement first gRPC methods: config validation and usage, listing and deleting workspaces (#37418)

* Implement first grpc methods: config validation and use, listing and deleting workspaces

* Add a state store to the default provider schema used in protocol 6 GRPCProvider tests

* Add `ValidateStateStoreConfig` test that shows diagnostics are returned as expected

* Add `ValidateStateStoreConfig` test that shows schema-related errors are returned before the `ValidateStateStoreConfig` RPC takes place

* Add similar tests for `ConfigureStateStore` - check errors are returned and schema errors are caught early

* Refactor to avoid looping over cases

* Update `GetStates` to check the store type is implemented before retrieving states. Add tests.

* Fix defect in DeleteState, update it to check state store type is valid

* Add tests for `DeleteState` method

---------

Co-authored-by: Radek Simko <radek.simko@gmail.com>
pull/37434/head
Sarah French 9 months ago committed by GitHub
parent 3b55d61ffa
commit 037ed81e9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1405,20 +1405,132 @@ func (p *GRPCProvider) ListResource(r providers.ListResourceRequest) providers.L
return resp
}
func (p *GRPCProvider) ValidateStateStoreConfig(r providers.ValidateStateStoreConfigRequest) providers.ValidateStateStoreConfigResponse {
panic("not implemented")
func (p *GRPCProvider) ValidateStateStoreConfig(r providers.ValidateStateStoreConfigRequest) (resp providers.ValidateStateStoreConfigResponse) {
logger.Trace("GRPCProvider.v6: ValidateStateStoreConfig")
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
return resp
}
ssSchema, ok := schema.StateStores[r.TypeName]
if !ok {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown state store type %q", r.TypeName))
return resp
}
mp, err := msgpack.Marshal(r.Config, ssSchema.Body.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
protoReq := &proto6.ValidateStateStore_Request{
TypeName: r.TypeName,
Config: &proto6.DynamicValue{Msgpack: mp},
}
protoResp, err := p.client.ValidateStateStoreConfig(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
return resp
}
func (p *GRPCProvider) ConfigureStateStore(r providers.ConfigureStateStoreRequest) providers.ConfigureStateStoreResponse {
panic("not implemented")
func (p *GRPCProvider) ConfigureStateStore(r providers.ConfigureStateStoreRequest) (resp providers.ConfigureStateStoreResponse) {
logger.Trace("GRPCProvider.v6: ConfigureStateStore")
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
return resp
}
ssSchema, ok := schema.StateStores[r.TypeName]
if !ok {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown state store type %q", r.TypeName))
return resp
}
mp, err := msgpack.Marshal(r.Config, ssSchema.Body.ImpliedType())
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(err)
return resp
}
protoReq := &proto6.ConfigureStateStore_Request{
TypeName: r.TypeName,
Config: &proto6.DynamicValue{
Msgpack: mp,
},
}
protoResp, err := p.client.ConfigureStateStore(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
return resp
}
func (p *GRPCProvider) GetStates(r providers.GetStatesRequest) providers.GetStatesResponse {
panic("not implemented")
func (p *GRPCProvider) GetStates(r providers.GetStatesRequest) (resp providers.GetStatesResponse) {
logger.Trace("GRPCProvider.v6: GetStates")
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
return resp
}
if _, ok := schema.StateStores[r.TypeName]; !ok {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown state store type %q", r.TypeName))
return resp
}
protoReq := &proto6.GetStates_Request{
TypeName: r.TypeName,
}
protoResp, err := p.client.GetStates(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
resp.States = protoResp.StateId
return resp
}
func (p *GRPCProvider) DeleteState(r providers.DeleteStateRequest) providers.DeleteStateResponse {
panic("not implemented")
func (p *GRPCProvider) DeleteState(r providers.DeleteStateRequest) (resp providers.DeleteStateResponse) {
logger.Trace("GRPCProvider.v6: DeleteState")
protoReq := &proto6.DeleteState_Request{
TypeName: r.TypeName,
}
schema := p.GetProviderSchema()
if schema.Diagnostics.HasErrors() {
resp.Diagnostics = schema.Diagnostics
return resp
}
if _, ok := schema.StateStores[r.TypeName]; !ok {
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown state store type %q", r.TypeName))
return resp
}
protoResp, err := p.client.DeleteState(p.ctx, protoReq)
if err != nil {
resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err))
return resp
}
resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics))
return resp
}
// closing the grpc connection is final, and terraform will call it at the end of every phase.

@ -223,6 +223,19 @@ func providerProtoSchema() *proto.GetProviderSchema_Response {
},
},
},
StateStoreSchemas: map[string]*proto.Schema{
"mock_store": {
Block: &proto.Schema_Block{
Version: 1,
Attributes: []*proto.Schema_Attribute{
{
Name: "region",
Type: []byte(`"string"`),
},
},
},
},
},
ServerCapabilities: &proto.ServerCapabilities{
GetProviderSchemaOptional: true,
},
@ -3036,3 +3049,435 @@ func TestGRPCProvider_invokeAction_linked_provider_returns_error(t *testing.T) {
checkDiagsHasError(t, evt.Diagnostics)
}
func TestGRPCProvider_ValidateStateStoreConfig_returns_validation_errors(t *testing.T) {
storeName := "mock_store" // mockProviderClient returns a mock that has this state store in its schemas
t.Run("no validation error raised", func(t *testing.T) {
typeName := storeName
var diagnostic []*proto.Diagnostic = nil
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
client.EXPECT().ValidateStateStoreConfig(
gomock.Any(),
gomock.Any(),
).Return(&proto.ValidateStateStore_Response{
Diagnostics: diagnostic,
}, nil)
request := providers.ValidateStateStoreConfigRequest{
TypeName: typeName,
Config: cty.ObjectVal(map[string]cty.Value{
"region": cty.StringVal("neptune"),
}),
}
// Act
resp := p.ValidateStateStoreConfig(request)
// Assert no error returned
checkDiags(t, resp.Diagnostics)
})
t.Run("validation error raised", func(t *testing.T) {
typeName := storeName
diagnostic := []*proto.Diagnostic{
{
Severity: proto.Diagnostic_ERROR,
Summary: "Error from ValidateStateStoreConfig",
Detail: "Something went wrong",
},
}
errorText := "Error from ValidateStateStoreConfig"
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
client.EXPECT().ValidateStateStoreConfig(
gomock.Any(),
gomock.Any(),
).Return(&proto.ValidateStateStore_Response{
Diagnostics: diagnostic,
}, nil)
request := providers.ValidateStateStoreConfigRequest{
TypeName: typeName,
Config: cty.ObjectVal(map[string]cty.Value{
"region": cty.StringVal("neptune"),
}),
}
// Act
resp := p.ValidateStateStoreConfig(request)
// Assert error returned
checkDiagsHasError(t, resp.Diagnostics)
if resp.Diagnostics[0].Description().Summary != errorText {
t.Fatalf("expected error summary to be %q, but got %q",
errorText,
resp.Diagnostics[0].Description().Summary,
)
}
})
}
func TestGRPCProvider_ValidateStateStoreConfig_schema_errors(t *testing.T) {
t.Run("no matching store type in provider", func(t *testing.T) {
typeName := "does_not_exist" // not present in mockProviderClient state store schemas
config := cty.EmptyObjectVal
expectedErrorSummary := "unknown state store type \"does_not_exist\""
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
request := providers.ValidateStateStoreConfigRequest{
TypeName: typeName,
Config: config,
}
// Act
resp := p.ValidateStateStoreConfig(request)
// Note - we haven't asserted that we expect ValidateStateStoreConfig
// to be called via the client; this package returns these errors before then.
// Assert that the expected error is returned
checkDiagsHasError(t, resp.Diagnostics)
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
t.Run("missing required attributes", func(t *testing.T) {
typeName := "mock_store" // Is present in mockProviderClient
config := cty.ObjectVal(map[string]cty.Value{
// Missing required `region` attr
})
expectedErrorSummary := "attribute \"region\" is required"
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
request := providers.ValidateStateStoreConfigRequest{
TypeName: typeName,
Config: config,
}
// Act
resp := p.ValidateStateStoreConfig(request)
// Note - we haven't asserted that we expect ValidateStateStoreConfig
// to be called via the client; this package returns these errors before then.
// Assert that the expected error is returned
checkDiagsHasError(t, resp.Diagnostics)
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
}
func TestGRPCProvider_ConfigureStateStore_returns_validation_errors(t *testing.T) {
storeName := "mock_store" // mockProviderClient returns a mock that has this state store in its schemas
t.Run("no validation error raised", func(t *testing.T) {
typeName := storeName
var diagnostic []*proto.Diagnostic = nil
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
client.EXPECT().ConfigureStateStore(
gomock.Any(),
gomock.Any(),
).Return(&proto.ConfigureStateStore_Response{
Diagnostics: diagnostic,
}, nil)
request := providers.ConfigureStateStoreRequest{
TypeName: typeName,
Config: cty.ObjectVal(map[string]cty.Value{
"region": cty.StringVal("neptune"),
}),
}
// Act
resp := p.ConfigureStateStore(request)
// Assert no error returned
checkDiags(t, resp.Diagnostics)
})
t.Run("validation error raised", func(t *testing.T) {
typeName := storeName
diagnostic := []*proto.Diagnostic{
{
Severity: proto.Diagnostic_ERROR,
Summary: "Error from ConfigureStateStore",
Detail: "Something went wrong",
},
}
errorText := "Error from ConfigureStateStore"
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
client.EXPECT().ConfigureStateStore(
gomock.Any(),
gomock.Any(),
).Return(&proto.ConfigureStateStore_Response{
Diagnostics: diagnostic,
}, nil)
request := providers.ConfigureStateStoreRequest{
TypeName: typeName,
Config: cty.ObjectVal(map[string]cty.Value{
"region": cty.StringVal("neptune"),
}),
}
// Act
resp := p.ConfigureStateStore(request)
// Assert whether error returned or not
checkDiagsHasError(t, resp.Diagnostics)
if resp.Diagnostics[0].Description().Summary != errorText {
t.Fatalf("expected error summary to be %q, but got %q",
errorText,
resp.Diagnostics[0].Description().Summary,
)
}
})
}
func TestGRPCProvider_ConfigureStateStore_schema_errors(t *testing.T) {
t.Run("no matching store type in provider", func(t *testing.T) {
typeName := "does_not_exist" // not present in mockProviderClient state store schemas
config := cty.EmptyObjectVal
expectedErrorSummary := "unknown state store type \"does_not_exist\""
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
request := providers.ConfigureStateStoreRequest{
TypeName: typeName,
Config: config,
}
// Act
resp := p.ConfigureStateStore(request)
// Note - we haven't asserted that we expect ConfigureStateStore
// to be called via the client; this package returns these errors before then.
// Assert that the expected error is returned
checkDiagsHasError(t, resp.Diagnostics)
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
t.Run("missing required attributes", func(t *testing.T) {
typeName := "mock_store" // Is present in mockProviderClient
config := cty.ObjectVal(map[string]cty.Value{
// Missing required `region` attr
})
expectedErrorSummary := "attribute \"region\" is required"
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
request := providers.ConfigureStateStoreRequest{
TypeName: typeName,
Config: config,
}
// Act
resp := p.ConfigureStateStore(request)
// Note - we haven't asserted that we expect ConfigureStateStore
// to be called via the client; this package returns these errors before then.
// Assert that the expected error is returned
checkDiagsHasError(t, resp.Diagnostics)
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
}
func TestGRPCProvider_GetStates(t *testing.T) {
t.Run("returns expected values", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
client.EXPECT().GetStates(
gomock.Any(),
gomock.Any(),
).Return(&proto.GetStates_Response{
StateId: []string{"default"},
Diagnostics: []*proto.Diagnostic{
{
Severity: proto.Diagnostic_ERROR,
Summary: "Error from GetStates",
Detail: "Something went wrong",
},
},
}, nil)
request := providers.GetStatesRequest{
TypeName: "mock_store",
}
// Act
resp := p.GetStates(request)
// Assert returned values
if len(resp.States) != 1 || resp.States[0] != "default" {
t.Fatalf("expected the returned states to be [\"default\"], instead got: %s", resp.States)
}
checkDiagsHasError(t, resp.Diagnostics)
expectedErrorSummary := "Error from GetStates"
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
t.Run("no matching store type in provider", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
request := providers.GetStatesRequest{
TypeName: "does_not_exist", // not present in mockProviderClient state store schemas
}
// Act
resp := p.GetStates(request)
checkDiagsHasError(t, resp.Diagnostics)
expectedErrorSummary := "unknown state store type \"does_not_exist\""
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
}
func TestGRPCProvider_DeleteState(t *testing.T) {
t.Run("returns expected values", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
client.EXPECT().DeleteState(
gomock.Any(),
gomock.Any(),
).Return(&proto.DeleteState_Response{
Diagnostics: []*proto.Diagnostic{
{
Severity: proto.Diagnostic_ERROR,
Summary: "Error from DeleteState",
Detail: "Something went wrong",
},
},
}, nil)
request := providers.DeleteStateRequest{
TypeName: "mock_store",
}
// Act
resp := p.DeleteState(request)
// Assert returned values
checkDiagsHasError(t, resp.Diagnostics)
expectedErrorSummary := "Error from DeleteState"
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
t.Run("no matching store type in provider", func(t *testing.T) {
client := mockProviderClient(t)
p := &GRPCProvider{
client: client,
ctx: context.Background(),
}
request := providers.DeleteStateRequest{
TypeName: "does_not_exist", // not present in mockProviderClient state store schemas
}
// Act
resp := p.DeleteState(request)
checkDiagsHasError(t, resp.Diagnostics)
expectedErrorSummary := "unknown state store type \"does_not_exist\""
if resp.Diagnostics[0].Description().Summary != expectedErrorSummary {
t.Fatalf("expected error summary to be %q, but got %q",
expectedErrorSummary,
resp.Diagnostics[0].Description().Summary,
)
}
})
}

Loading…
Cancel
Save