Storage: create a client with access to all interfaces, not just ResourceStore (#92967)

This commit is contained in:
Ryan McKinley 2024-09-05 12:52:30 +03:00 committed by GitHub
parent 9338e40dc3
commit 5441e4c752
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 95 additions and 118 deletions

View File

@ -41,7 +41,7 @@ func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter ge
if err != nil {
return nil, err
}
client := resource.NewLocalResourceStoreClient(server)
client := resource.NewLocalResourceClient(server)
optsGetter := apistore.NewRESTOptionsGetterForClient(client,
defaultOpts.StorageConfig.Config,
)

View File

@ -298,7 +298,7 @@ func (s *service) start(ctx context.Context) error {
if err != nil {
return err
}
client := resource.NewLocalResourceStoreClient(server)
client := resource.NewLocalResourceClient(server)
serverConfig.Config.RESTOptionsGetter = apistore.NewRESTOptionsGetterForClient(client,
o.RecommendedOptions.Etcd.StorageConfig)
@ -314,7 +314,7 @@ func (s *service) start(ctx context.Context) error {
}
// Create a client instance
client := resource.NewResourceStoreClientGRPC(conn)
client := resource.NewResourceClient(conn)
serverConfig.Config.RESTOptionsGetter = apistore.NewRESTOptionsGetterForClient(client, o.RecommendedOptions.Etcd.StorageConfig)
case grafanaapiserveroptions.StorageTypeLegacy:

View File

@ -50,7 +50,7 @@ func NewRESTOptionsGetterMemory(originalStorageConfig storagebackend.Config) (*R
return nil, err
}
return NewRESTOptionsGetterForClient(
resource.NewLocalResourceStoreClient(server),
resource.NewLocalResourceClient(server),
originalStorageConfig,
), nil
}
@ -84,7 +84,7 @@ func NewRESTOptionsGetterForFile(path string,
return nil, err
}
return NewRESTOptionsGetterForClient(
resource.NewLocalResourceStoreClient(server),
resource.NewLocalResourceClient(server),
originalStorageConfig,
), nil
}

View File

@ -97,7 +97,7 @@ func testSetup(t testing.TB, opts ...setupOption) (context.Context, storage.Inte
Backend: backend,
})
require.NoError(t, err)
client := resource.NewLocalResourceStoreClient(server)
client := resource.NewLocalResourceClient(server)
config := storagebackend.NewDefaultConfig(setupOpts.prefix, setupOpts.codec)
store, destroyFunc, err := NewStorage(

View File

@ -0,0 +1,54 @@
package resource
import (
"github.com/fullstorydev/grpchan"
"github.com/fullstorydev/grpchan/inprocgrpc"
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"google.golang.org/grpc"
grpcUtils "github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
)
type ResourceClient interface {
ResourceStoreClient
ResourceIndexClient
DiagnosticsClient
}
// Internal implementation
type resourceClient struct {
ResourceStoreClient
ResourceIndexClient
DiagnosticsClient
}
func NewResourceClient(channel *grpc.ClientConn) ResourceClient {
cc := grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor)
return &resourceClient{
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
}
}
func NewLocalResourceClient(server ResourceServer) ResourceClient {
channel := &inprocgrpc.Channel{}
auth := &grpcUtils.Authenticator{}
channel.RegisterService(
grpchan.InterceptServer(
&ResourceStore_ServiceDesc,
grpcAuth.UnaryServerInterceptor(auth.Authenticate),
grpcAuth.StreamServerInterceptor(auth.Authenticate),
),
server, // Implements all the things
)
cc := grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor)
return &resourceClient{
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
}
}

View File

@ -1,30 +0,0 @@
package resource
import (
"github.com/fullstorydev/grpchan"
"github.com/fullstorydev/grpchan/inprocgrpc"
grpcAuth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"google.golang.org/grpc"
grpcUtils "github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
)
func NewLocalResourceStoreClient(server ResourceStoreServer) ResourceStoreClient {
channel := &inprocgrpc.Channel{}
auth := &grpcUtils.Authenticator{}
channel.RegisterService(
grpchan.InterceptServer(
&ResourceStore_ServiceDesc,
grpcAuth.UnaryServerInterceptor(auth.Authenticate),
grpcAuth.StreamServerInterceptor(auth.Authenticate),
),
server,
)
return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}
func NewResourceStoreClientGRPC(channel *grpc.ClientConn) ResourceStoreClient {
return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}

View File

@ -1951,7 +1951,7 @@ type OriginResponse struct {
// ResourceVersion of the list response
ResourceVersion int64 `protobuf:"varint,3,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
// Error details
Error *ErrorResult `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"`
Error *ErrorResult `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
}
func (x *OriginResponse) Reset() {
@ -2403,7 +2403,7 @@ var file_resource_proto_rawDesc = []byte{
0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x12, 0x2b, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72,
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x2e, 0x0a,
0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75,
@ -2446,29 +2446,25 @@ var file_resource_proto_rawDesc = []byte{
0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63,
0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01,
0x32, 0xc3, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64,
0x65, 0x78, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61,
0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73,
0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19,
0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72,
0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69,
0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f,
0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72,
0x32, 0x8c, 0x01, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64,
0x65, 0x78, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e, 0x72,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f,
0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74,
0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65,
0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c,
0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42,
0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72,
0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b,
0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65,
0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32,
0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48,
0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65,
0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67,
0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61,
0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -2554,22 +2550,20 @@ var file_resource_proto_depIdxs = []int32{
13, // 32: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
19, // 33: resource.ResourceStore.List:input_type -> resource.ListRequest
21, // 34: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
15, // 35: resource.ResourceIndex.Read:input_type -> resource.ReadRequest
23, // 36: resource.ResourceIndex.History:input_type -> resource.HistoryRequest
25, // 37: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest
28, // 38: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
16, // 39: resource.ResourceStore.Read:output_type -> resource.ReadResponse
10, // 40: resource.ResourceStore.Create:output_type -> resource.CreateResponse
12, // 41: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
14, // 42: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
20, // 43: resource.ResourceStore.List:output_type -> resource.ListResponse
22, // 44: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
16, // 45: resource.ResourceIndex.Read:output_type -> resource.ReadResponse
24, // 46: resource.ResourceIndex.History:output_type -> resource.HistoryResponse
27, // 47: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse
29, // 48: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
39, // [39:49] is the sub-list for method output_type
29, // [29:39] is the sub-list for method input_type
23, // 35: resource.ResourceIndex.History:input_type -> resource.HistoryRequest
25, // 36: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest
28, // 37: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
16, // 38: resource.ResourceStore.Read:output_type -> resource.ReadResponse
10, // 39: resource.ResourceStore.Create:output_type -> resource.CreateResponse
12, // 40: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
14, // 41: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
20, // 42: resource.ResourceStore.List:output_type -> resource.ListResponse
22, // 43: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
24, // 44: resource.ResourceIndex.History:output_type -> resource.HistoryResponse
27, // 45: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse
29, // 46: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
38, // [38:47] is the sub-list for method output_type
29, // [29:38] is the sub-list for method input_type
29, // [29:29] is the sub-list for extension type_name
29, // [29:29] is the sub-list for extension extendee
0, // [0:29] is the sub-list for field type_name

View File

@ -442,8 +442,6 @@ service ResourceStore {
service ResourceIndex {
// TODO: rpc Search(...) ... eventually a typed response
rpc Read(ReadRequest) returns (ReadResponse); // Duplicated -- for client read only usage
// Show resource history (and trash)
rpc History(HistoryRequest) returns (HistoryResponse);

View File

@ -348,7 +348,6 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{
}
const (
ResourceIndex_Read_FullMethodName = "/resource.ResourceIndex/Read"
ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History"
ResourceIndex_Origin_FullMethodName = "/resource.ResourceIndex/Origin"
)
@ -360,7 +359,6 @@ const (
// Unlike the ResourceStore, this service can be exposed to clients directly
// It should be implemented with efficient indexes and does not need read-after-write semantics
type ResourceIndexClient interface {
Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
// Show resource history (and trash)
History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error)
// Used for efficient provisioning
@ -375,16 +373,6 @@ func NewResourceIndexClient(cc grpc.ClientConnInterface) ResourceIndexClient {
return &resourceIndexClient{cc}
}
func (c *resourceIndexClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ReadResponse)
err := c.cc.Invoke(ctx, ResourceIndex_Read_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HistoryResponse)
@ -412,7 +400,6 @@ func (c *resourceIndexClient) Origin(ctx context.Context, in *OriginRequest, opt
// Unlike the ResourceStore, this service can be exposed to clients directly
// It should be implemented with efficient indexes and does not need read-after-write semantics
type ResourceIndexServer interface {
Read(context.Context, *ReadRequest) (*ReadResponse, error)
// Show resource history (and trash)
History(context.Context, *HistoryRequest) (*HistoryResponse, error)
// Used for efficient provisioning
@ -423,9 +410,6 @@ type ResourceIndexServer interface {
type UnimplementedResourceIndexServer struct {
}
func (UnimplementedResourceIndexServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
}
func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method History not implemented")
}
@ -444,24 +428,6 @@ func RegisterResourceIndexServer(s grpc.ServiceRegistrar, srv ResourceIndexServe
s.RegisterService(&ResourceIndex_ServiceDesc, srv)
}
func _ResourceIndex_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReadRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ResourceIndexServer).Read(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ResourceIndex_Read_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ResourceIndexServer).Read(ctx, req.(*ReadRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HistoryRequest)
if err := dec(in); err != nil {
@ -505,10 +471,6 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
ServiceName: "resource.ResourceIndex",
HandlerType: (*ResourceIndexServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Read",
Handler: _ResourceIndex_Read_Handler,
},
{
MethodName: "History",
Handler: _ResourceIndex_History_Handler,

View File

@ -25,7 +25,6 @@ type ResourceServer interface {
ResourceStoreServer
ResourceIndexServer
DiagnosticsServer
LifecycleHooks
}
type ListIterator interface {

View File

@ -356,7 +356,7 @@ func TestClientServer(t *testing.T) {
t.Run("Create a client", func(t *testing.T) {
conn, err := grpc.NewClient(svc.GetAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
client = resource.NewResourceStoreClientGRPC(conn)
client = resource.NewResourceClient(conn)
})
t.Run("Create a resource", func(t *testing.T) {