diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index 3f3abf3edf..e7c46797b4 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -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. diff --git a/internal/plugin6/grpc_provider_test.go b/internal/plugin6/grpc_provider_test.go index eaef559c70..cc5a4251ce 100644 --- a/internal/plugin6/grpc_provider_test.go +++ b/internal/plugin6/grpc_provider_test.go @@ -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, + ) + } + }) +}