mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Storage: add an admin write flavor that can explicitly set the user/time (#58618)
This commit is contained in:
parent
69b5a9c752
commit
5934407443
@ -123,14 +123,21 @@ func (e *objectStoreJob) start(ctx context.Context) {
|
||||
rowUser.UserID = 0 // avoid Uint64Val issue????
|
||||
}
|
||||
|
||||
_, err = e.store.Write(ctx, &object.WriteObjectRequest{
|
||||
_, err = e.store.AdminWrite(ctx, &object.AdminWriteObjectRequest{
|
||||
GRN: &object.GRN{
|
||||
Scope: models.ObjectStoreScopeEntity,
|
||||
UID: dash.UID,
|
||||
Kind: models.StandardKindDashboard,
|
||||
},
|
||||
Body: dash.Body,
|
||||
Comment: "export from dashboard table",
|
||||
ClearHistory: true,
|
||||
Version: fmt.Sprintf("%d", dash.Version),
|
||||
CreatedAt: dash.Created.UnixMilli(),
|
||||
UpdatedAt: dash.Updated.UnixMilli(),
|
||||
UpdatedBy: fmt.Sprintf("user:%d", dash.UpdatedBy),
|
||||
CreatedBy: fmt.Sprintf("user:%d", dash.CreatedBy),
|
||||
Origin: "export-from-sql",
|
||||
Body: dash.Data,
|
||||
Comment: "(exported from SQL)",
|
||||
})
|
||||
if err != nil {
|
||||
e.status.Status = "error: " + err.Error()
|
||||
@ -254,34 +261,25 @@ func (e *objectStoreJob) start(ctx context.Context) {
|
||||
}
|
||||
|
||||
type dashInfo struct {
|
||||
OrgID int64
|
||||
OrgID int64 `db:"org_id"`
|
||||
UID string
|
||||
Body []byte
|
||||
UpdatedBy int64
|
||||
Version int64
|
||||
Slug string
|
||||
Data []byte
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
CreatedBy int64 `db:"created_by"`
|
||||
UpdatedBy int64 `db:"updated_by"`
|
||||
}
|
||||
|
||||
// TODO, paging etc
|
||||
func (e *objectStoreJob) getDashboards(ctx context.Context) ([]dashInfo, error) {
|
||||
e.status.Last = "find dashbaords...."
|
||||
e.broadcaster(e.status)
|
||||
|
||||
dash := make([]dashInfo, 0)
|
||||
rows, err := e.sess.Query(ctx, "SELECT org_id,uid,data,updated_by FROM dashboard WHERE is_folder=false")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
if e.stopRequested {
|
||||
return dash, nil
|
||||
}
|
||||
|
||||
row := dashInfo{}
|
||||
err = rows.Scan(&row.OrgID, &row.UID, &row.Body, &row.UpdatedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dash = append(dash, row)
|
||||
}
|
||||
return dash, nil
|
||||
err := e.sess.Select(ctx, &dash, "SELECT org_id,uid,version,slug,data,created,updated,created_by,updated_by FROM dashboard WHERE is_folder=false")
|
||||
return dash, err
|
||||
}
|
||||
|
||||
func (e *objectStoreJob) getStatus() ExportStatus {
|
||||
|
@ -46,9 +46,9 @@ func addObjectStorageMigrations(mg *migrator.Migrator) {
|
||||
{Name: "updated_by", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
{Name: "created_by", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||
|
||||
// For objects that are synchronized from an external source (ie provisioning or git)
|
||||
{Name: "sync_src", Type: migrator.DB_Text, Nullable: true},
|
||||
{Name: "sync_time", Type: migrator.DB_BigInt, Nullable: true},
|
||||
// Mark objects with origin metadata
|
||||
{Name: "origin", Type: migrator.DB_Text, Nullable: true},
|
||||
{Name: "origin_ts", Type: migrator.DB_BigInt, Nullable: false},
|
||||
|
||||
// Summary data (always extracted from the `body` column)
|
||||
{Name: "name", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
|
||||
@ -134,7 +134,7 @@ func addObjectStorageMigrations(mg *migrator.Migrator) {
|
||||
// Migration cleanups: given that this is a complex setup
|
||||
// that requires a lot of testing before we are ready to push out of dev
|
||||
// this script lets us easy wipe previous changes and initialize clean tables
|
||||
suffix := " (v2)" // change this when we want to wipe and reset the object tables
|
||||
suffix := " (v5)" // change this when we want to wipe and reset the object tables
|
||||
mg.AddMigration("ObjectStore init: cleanup"+suffix, migrator.NewRawSQLMigration(strings.TrimSpace(`
|
||||
DELETE FROM migration_log WHERE migration_id LIKE 'ObjectStore init%';
|
||||
`)))
|
||||
|
@ -35,6 +35,10 @@ var (
|
||||
rawObjectVersion = 9
|
||||
)
|
||||
|
||||
// Make sure we implement both store + admin
|
||||
var _ object.ObjectStoreServer = &dummyObjectServer{}
|
||||
var _ object.ObjectStoreAdminServer = &dummyObjectServer{}
|
||||
|
||||
func ProvideDummyObjectServer(cfg *setting.Cfg, grpcServerProvider grpcserver.Provider, kinds kind.KindRegistry) object.ObjectStoreServer {
|
||||
objectServer := &dummyObjectServer{
|
||||
collection: persistentcollection.NewLocalFSPersistentCollection[*RawObjectWithHistory]("raw-object", cfg.DataPath, rawObjectVersion),
|
||||
@ -149,7 +153,7 @@ func createContentsHash(contents []byte) string {
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (i *dummyObjectServer) update(ctx context.Context, r *object.WriteObjectRequest, namespace string) (*object.WriteObjectResponse, error) {
|
||||
func (i *dummyObjectServer) update(ctx context.Context, r *object.AdminWriteObjectRequest, namespace string) (*object.WriteObjectResponse, error) {
|
||||
builder := i.kinds.GetSummaryBuilder(r.GRN.Kind)
|
||||
if builder == nil {
|
||||
return nil, fmt.Errorf("unsupported kind: " + r.GRN.Kind)
|
||||
@ -222,7 +226,7 @@ func (i *dummyObjectServer) update(ctx context.Context, r *object.WriteObjectReq
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func (i *dummyObjectServer) insert(ctx context.Context, r *object.WriteObjectRequest, namespace string) (*object.WriteObjectResponse, error) {
|
||||
func (i *dummyObjectServer) insert(ctx context.Context, r *object.AdminWriteObjectRequest, namespace string) (*object.WriteObjectResponse, error) {
|
||||
modifier := store.GetUserIDString(store.UserFromContext(ctx))
|
||||
rawObj := &object.RawObject{
|
||||
GRN: r.GRN,
|
||||
@ -266,6 +270,15 @@ func (i *dummyObjectServer) insert(ctx context.Context, r *object.WriteObjectReq
|
||||
}
|
||||
|
||||
func (i *dummyObjectServer) Write(ctx context.Context, r *object.WriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
return i.doWrite(ctx, object.ToAdminWriteObjectRequest(r))
|
||||
}
|
||||
|
||||
func (i *dummyObjectServer) AdminWrite(ctx context.Context, r *object.AdminWriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
// Check permissions?
|
||||
return i.doWrite(ctx, r)
|
||||
}
|
||||
|
||||
func (i *dummyObjectServer) doWrite(ctx context.Context, r *object.AdminWriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
grn := getFullGRN(ctx, r.GRN)
|
||||
namespace := namespaceFromUID(grn)
|
||||
obj, err := i.collection.FindFirst(ctx, namespace, func(i *RawObjectWithHistory) (bool, error) {
|
||||
|
@ -7,12 +7,20 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/store/object"
|
||||
)
|
||||
|
||||
// Make sure we implement both store + admin
|
||||
var _ object.ObjectStoreServer = &fakeObjectStore{}
|
||||
var _ object.ObjectStoreAdminServer = &fakeObjectStore{}
|
||||
|
||||
func ProvideFakeObjectServer() object.ObjectStoreServer {
|
||||
return &fakeObjectStore{}
|
||||
}
|
||||
|
||||
type fakeObjectStore struct{}
|
||||
|
||||
func (i fakeObjectStore) AdminWrite(ctx context.Context, r *object.AdminWriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
func (i fakeObjectStore) Write(ctx context.Context, r *object.WriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
@ -102,10 +102,10 @@ func (codec *rawObjectCodec) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream)
|
||||
stream.WriteInt64(obj.Size)
|
||||
}
|
||||
|
||||
if obj.Sync != nil {
|
||||
if obj.Origin != nil {
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField("sync")
|
||||
stream.WriteVal(obj.Sync)
|
||||
stream.WriteObjectField("origin")
|
||||
stream.WriteVal(obj.Origin)
|
||||
}
|
||||
|
||||
stream.WriteObjectEnd()
|
||||
@ -137,9 +137,9 @@ func readRawObject(iter *jsoniter.Iterator, raw *RawObject) {
|
||||
raw.ETag = iter.ReadString()
|
||||
case "version":
|
||||
raw.Version = iter.ReadString()
|
||||
case "sync":
|
||||
raw.Sync = &RawObjectSyncInfo{}
|
||||
iter.ReadVal(raw.Sync)
|
||||
case "origin":
|
||||
raw.Origin = &ObjectOriginInfo{}
|
||||
iter.ReadVal(raw.Origin)
|
||||
|
||||
case "body":
|
||||
var val interface{}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -53,10 +53,10 @@ message RawObject {
|
||||
string version = 9;
|
||||
|
||||
// External location info
|
||||
RawObjectSyncInfo sync = 10;
|
||||
ObjectOriginInfo origin = 10;
|
||||
}
|
||||
|
||||
message RawObjectSyncInfo {
|
||||
message ObjectOriginInfo {
|
||||
// NOTE: currently managed by the dashboard_provisioning table
|
||||
string source = 1;
|
||||
|
||||
@ -156,6 +156,51 @@ message WriteObjectRequest {
|
||||
string previous_version = 4;
|
||||
}
|
||||
|
||||
// This operation is useful when syncing a resource from external sources
|
||||
// that have more accurate metadata information (git, or an archive).
|
||||
// This process can bypass the forced checks that
|
||||
message AdminWriteObjectRequest {
|
||||
// Object identifier
|
||||
GRN GRN = 1;
|
||||
|
||||
// The raw object body
|
||||
bytes body = 2;
|
||||
|
||||
// Message that can be seen when exploring object history
|
||||
string comment = 3;
|
||||
|
||||
// Time in epoch milliseconds that the object was created
|
||||
// Optional, if 0 it will use the current time
|
||||
int64 created_at = 4;
|
||||
|
||||
// Time in epoch milliseconds that the object was updated
|
||||
// Optional, if empty it will use the current user
|
||||
int64 updated_at = 5;
|
||||
|
||||
// Who created the object
|
||||
// Optional, if 0 it will use the current time
|
||||
string created_by = 6;
|
||||
|
||||
// Who updated the object
|
||||
// Optional, if empty it will use the current user
|
||||
string updated_by = 7;
|
||||
|
||||
// An explicit version identifier
|
||||
// Optional, if set, this will overwrite/define an explicit version
|
||||
string version = 8;
|
||||
|
||||
// Used for optimistic locking. If missing, the previous version will be replaced regardless
|
||||
// This may not be used along with an explicit version in the request
|
||||
string previous_version = 9;
|
||||
|
||||
// Request that all previous versions are removed from the history
|
||||
// This will make sense for systems that manage history explicitly externallay
|
||||
bool clear_history = 10;
|
||||
|
||||
// Optionally define where the object came from
|
||||
string origin = 11;
|
||||
}
|
||||
|
||||
message WriteObjectResponse {
|
||||
// Error info -- if exists, the save did not happen
|
||||
ObjectErrorInfo error = 1;
|
||||
@ -312,8 +357,7 @@ message ObjectSearchResponse {
|
||||
// Storage interface
|
||||
//-----------------------------------------------
|
||||
|
||||
// This assumes a future grpc interface where the user info is passed in context, not in each message body
|
||||
// for now it will only work with an admin API key
|
||||
// The object store provides a basic CRUD (+watch eventually) interface for generic objects
|
||||
service ObjectStore {
|
||||
rpc Read(ReadObjectRequest) returns (ReadObjectResponse);
|
||||
rpc BatchRead(BatchReadObjectRequest) returns (BatchReadObjectResponse);
|
||||
@ -325,4 +369,13 @@ service ObjectStore {
|
||||
// Ideally an additional search endpoint with more flexibility to limit what you actually care about
|
||||
// https://github.com/grafana/grafana-plugin-sdk-go/blob/main/proto/backend.proto#L129
|
||||
// rpc SearchEX(ObjectSearchRequest) returns (DataResponse);
|
||||
|
||||
// TEMPORARY... while we split this into a new service (see below)
|
||||
rpc AdminWrite(AdminWriteObjectRequest) returns (WriteObjectResponse);
|
||||
}
|
||||
|
||||
// The admin service extends the basic object store interface, but provides
|
||||
// more explicit control that can support bulk operations like efficient git sync
|
||||
service ObjectStoreAdmin {
|
||||
rpc AdminWrite(AdminWriteObjectRequest) returns (WriteObjectResponse);
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ type ObjectStoreClient interface {
|
||||
Delete(ctx context.Context, in *DeleteObjectRequest, opts ...grpc.CallOption) (*DeleteObjectResponse, error)
|
||||
History(ctx context.Context, in *ObjectHistoryRequest, opts ...grpc.CallOption) (*ObjectHistoryResponse, error)
|
||||
Search(ctx context.Context, in *ObjectSearchRequest, opts ...grpc.CallOption) (*ObjectSearchResponse, error)
|
||||
// TEMPORARY... while we split this into a new service (see below)
|
||||
AdminWrite(ctx context.Context, in *AdminWriteObjectRequest, opts ...grpc.CallOption) (*WriteObjectResponse, error)
|
||||
}
|
||||
|
||||
type objectStoreClient struct {
|
||||
@ -92,6 +94,15 @@ func (c *objectStoreClient) Search(ctx context.Context, in *ObjectSearchRequest,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *objectStoreClient) AdminWrite(ctx context.Context, in *AdminWriteObjectRequest, opts ...grpc.CallOption) (*WriteObjectResponse, error) {
|
||||
out := new(WriteObjectResponse)
|
||||
err := c.cc.Invoke(ctx, "/object.ObjectStore/AdminWrite", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ObjectStoreServer is the server API for ObjectStore service.
|
||||
// All implementations should embed UnimplementedObjectStoreServer
|
||||
// for forward compatibility
|
||||
@ -102,6 +113,8 @@ type ObjectStoreServer interface {
|
||||
Delete(context.Context, *DeleteObjectRequest) (*DeleteObjectResponse, error)
|
||||
History(context.Context, *ObjectHistoryRequest) (*ObjectHistoryResponse, error)
|
||||
Search(context.Context, *ObjectSearchRequest) (*ObjectSearchResponse, error)
|
||||
// TEMPORARY... while we split this into a new service (see below)
|
||||
AdminWrite(context.Context, *AdminWriteObjectRequest) (*WriteObjectResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedObjectStoreServer should be embedded to have forward compatible implementations.
|
||||
@ -126,6 +139,9 @@ func (UnimplementedObjectStoreServer) History(context.Context, *ObjectHistoryReq
|
||||
func (UnimplementedObjectStoreServer) Search(context.Context, *ObjectSearchRequest) (*ObjectSearchResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Search not implemented")
|
||||
}
|
||||
func (UnimplementedObjectStoreServer) AdminWrite(context.Context, *AdminWriteObjectRequest) (*WriteObjectResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminWrite not implemented")
|
||||
}
|
||||
|
||||
// UnsafeObjectStoreServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ObjectStoreServer will
|
||||
@ -246,6 +262,24 @@ func _ObjectStore_Search_Handler(srv interface{}, ctx context.Context, dec func(
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ObjectStore_AdminWrite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminWriteObjectRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ObjectStoreServer).AdminWrite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/object.ObjectStore/AdminWrite",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ObjectStoreServer).AdminWrite(ctx, req.(*AdminWriteObjectRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ObjectStore_ServiceDesc is the grpc.ServiceDesc for ObjectStore service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -277,6 +311,94 @@ var ObjectStore_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Search",
|
||||
Handler: _ObjectStore_Search_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminWrite",
|
||||
Handler: _ObjectStore_AdminWrite_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "object.proto",
|
||||
}
|
||||
|
||||
// ObjectStoreAdminClient is the client API for ObjectStoreAdmin service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ObjectStoreAdminClient interface {
|
||||
AdminWrite(ctx context.Context, in *AdminWriteObjectRequest, opts ...grpc.CallOption) (*WriteObjectResponse, error)
|
||||
}
|
||||
|
||||
type objectStoreAdminClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewObjectStoreAdminClient(cc grpc.ClientConnInterface) ObjectStoreAdminClient {
|
||||
return &objectStoreAdminClient{cc}
|
||||
}
|
||||
|
||||
func (c *objectStoreAdminClient) AdminWrite(ctx context.Context, in *AdminWriteObjectRequest, opts ...grpc.CallOption) (*WriteObjectResponse, error) {
|
||||
out := new(WriteObjectResponse)
|
||||
err := c.cc.Invoke(ctx, "/object.ObjectStoreAdmin/AdminWrite", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ObjectStoreAdminServer is the server API for ObjectStoreAdmin service.
|
||||
// All implementations should embed UnimplementedObjectStoreAdminServer
|
||||
// for forward compatibility
|
||||
type ObjectStoreAdminServer interface {
|
||||
AdminWrite(context.Context, *AdminWriteObjectRequest) (*WriteObjectResponse, error)
|
||||
}
|
||||
|
||||
// UnimplementedObjectStoreAdminServer should be embedded to have forward compatible implementations.
|
||||
type UnimplementedObjectStoreAdminServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedObjectStoreAdminServer) AdminWrite(context.Context, *AdminWriteObjectRequest) (*WriteObjectResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method AdminWrite not implemented")
|
||||
}
|
||||
|
||||
// UnsafeObjectStoreAdminServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ObjectStoreAdminServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeObjectStoreAdminServer interface {
|
||||
mustEmbedUnimplementedObjectStoreAdminServer()
|
||||
}
|
||||
|
||||
func RegisterObjectStoreAdminServer(s grpc.ServiceRegistrar, srv ObjectStoreAdminServer) {
|
||||
s.RegisterService(&ObjectStoreAdmin_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ObjectStoreAdmin_AdminWrite_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminWriteObjectRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ObjectStoreAdminServer).AdminWrite(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/object.ObjectStoreAdmin/AdminWrite",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ObjectStoreAdminServer).AdminWrite(ctx, req.(*AdminWriteObjectRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ObjectStoreAdmin_ServiceDesc is the grpc.ServiceDesc for ObjectStoreAdmin service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ObjectStoreAdmin_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "object.ObjectStoreAdmin",
|
||||
HandlerType: (*ObjectStoreAdminServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "AdminWrite",
|
||||
Handler: _ObjectStoreAdmin_AdminWrite_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "object.proto",
|
||||
|
@ -23,6 +23,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// Make sure we implement both store + admin
|
||||
var _ object.ObjectStoreServer = &sqlObjectServer{}
|
||||
var _ object.ObjectStoreAdminServer = &sqlObjectServer{}
|
||||
|
||||
func ProvideSQLObjectServer(db db.DB, cfg *setting.Cfg, grpcServerProvider grpcserver.Provider, kinds kind.KindRegistry, resolver resolver.ObjectReferenceResolver) object.ObjectStoreServer {
|
||||
objectServer := &sqlObjectServer{
|
||||
sess: db.GetSqlxSession(),
|
||||
@ -49,7 +53,7 @@ func getReadSelect(r *object.ReadObjectRequest) string {
|
||||
"size", "etag", "errors", // errors are always returned
|
||||
"created_at", "created_by",
|
||||
"updated_at", "updated_by",
|
||||
"sync_src", "sync_time"}
|
||||
"origin", "origin_ts"}
|
||||
|
||||
if r.WithBody {
|
||||
fields = append(fields, `body`)
|
||||
@ -62,8 +66,8 @@ func getReadSelect(r *object.ReadObjectRequest) string {
|
||||
|
||||
func (s *sqlObjectServer) rowToReadObjectResponse(ctx context.Context, rows *sql.Rows, r *object.ReadObjectRequest) (*object.ReadObjectResponse, error) {
|
||||
path := "" // string (extract UID?)
|
||||
var syncSrc sql.NullString
|
||||
var syncTime sql.NullTime
|
||||
var origin sql.NullString
|
||||
originTime := int64(0)
|
||||
raw := &object.RawObject{
|
||||
GRN: &object.GRN{},
|
||||
}
|
||||
@ -74,7 +78,7 @@ func (s *sqlObjectServer) rowToReadObjectResponse(ctx context.Context, rows *sql
|
||||
&raw.Size, &raw.ETag, &summaryjson.errors,
|
||||
&raw.CreatedAt, &raw.CreatedBy,
|
||||
&raw.UpdatedAt, &raw.UpdatedBy,
|
||||
&syncSrc, &syncTime,
|
||||
&origin, &originTime,
|
||||
}
|
||||
if r.WithBody {
|
||||
args = append(args, &raw.Body)
|
||||
@ -88,10 +92,10 @@ func (s *sqlObjectServer) rowToReadObjectResponse(ctx context.Context, rows *sql
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if syncSrc.Valid || syncTime.Valid {
|
||||
raw.Sync = &object.RawObjectSyncInfo{
|
||||
Source: syncSrc.String,
|
||||
Time: syncTime.Time.UnixMilli(),
|
||||
if origin.Valid {
|
||||
raw.Origin = &object.ObjectOriginInfo{
|
||||
Source: origin.String,
|
||||
Time: originTime,
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,6 +277,11 @@ func (s *sqlObjectServer) BatchRead(ctx context.Context, b *object.BatchReadObje
|
||||
}
|
||||
|
||||
func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
return s.AdminWrite(ctx, object.ToAdminWriteObjectRequest(r))
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (s *sqlObjectServer) AdminWrite(ctx context.Context, r *object.AdminWriteObjectRequest) (*object.WriteObjectResponse, error) {
|
||||
route, err := s.getObjectKey(ctx, r.GRN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -282,9 +291,20 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
return nil, fmt.Errorf("invalid grn")
|
||||
}
|
||||
|
||||
modifier := store.UserFromContext(ctx)
|
||||
if modifier == nil {
|
||||
return nil, fmt.Errorf("can not find user in context")
|
||||
timestamp := time.Now().UnixMilli()
|
||||
createdAt := r.CreatedAt
|
||||
createdBy := r.CreatedBy
|
||||
updatedAt := r.UpdatedAt
|
||||
updatedBy := r.UpdatedBy
|
||||
if updatedBy == "" {
|
||||
modifier := store.UserFromContext(ctx)
|
||||
if modifier == nil {
|
||||
return nil, fmt.Errorf("can not find user in context")
|
||||
}
|
||||
updatedBy = store.GetUserIDString(modifier)
|
||||
}
|
||||
if updatedAt < 1000 {
|
||||
updatedAt = timestamp
|
||||
}
|
||||
|
||||
summary, body, err := s.prepare(ctx, r)
|
||||
@ -309,10 +329,26 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
}
|
||||
|
||||
err = s.sess.WithTransaction(ctx, func(tx *session.SessionTx) error {
|
||||
var versionInfo *object.ObjectVersionInfo
|
||||
isUpdate := false
|
||||
versionInfo, err := s.selectForUpdate(ctx, tx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
if r.ClearHistory {
|
||||
// Optionally keep the original creation time information
|
||||
if createdAt < 1000 || createdBy == "" {
|
||||
err = s.fillCreationInfo(ctx, tx, path, &createdAt, &createdBy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = doDelete(ctx, tx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
versionInfo = &object.ObjectVersionInfo{}
|
||||
} else {
|
||||
versionInfo, err = s.selectForUpdate(ctx, tx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Same object
|
||||
@ -330,18 +366,21 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
}
|
||||
|
||||
// Set the comment on this write
|
||||
timestamp := time.Now().UnixMilli()
|
||||
versionInfo.Comment = r.Comment
|
||||
if versionInfo.Version == "" {
|
||||
versionInfo.Version = "1"
|
||||
} else {
|
||||
// Increment the version
|
||||
i, _ := strconv.ParseInt(versionInfo.Version, 0, 64)
|
||||
if i < 1 {
|
||||
i = timestamp
|
||||
if r.Version == "" {
|
||||
if versionInfo.Version == "" {
|
||||
versionInfo.Version = "1"
|
||||
} else {
|
||||
// Increment the version
|
||||
i, _ := strconv.ParseInt(versionInfo.Version, 0, 64)
|
||||
if i < 1 {
|
||||
i = timestamp
|
||||
}
|
||||
versionInfo.Version = fmt.Sprintf("%d", i+1)
|
||||
isUpdate = true
|
||||
}
|
||||
versionInfo.Version = fmt.Sprintf("%d", i+1)
|
||||
isUpdate = true
|
||||
} else {
|
||||
versionInfo.Version = r.Version
|
||||
}
|
||||
|
||||
if isUpdate {
|
||||
@ -357,8 +396,8 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
// 1. Add the `object_history` values
|
||||
versionInfo.Size = int64(len(body))
|
||||
versionInfo.ETag = etag
|
||||
versionInfo.UpdatedAt = timestamp
|
||||
versionInfo.UpdatedBy = store.GetUserIDString(modifier)
|
||||
versionInfo.UpdatedAt = updatedAt
|
||||
versionInfo.UpdatedBy = updatedBy
|
||||
_, err = tx.Exec(ctx, `INSERT INTO object_history (`+
|
||||
"path, version, message, "+
|
||||
"size, body, etag, "+
|
||||
@ -366,7 +405,7 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
path, versionInfo.Version, versionInfo.Comment,
|
||||
versionInfo.Size, body, versionInfo.ETag,
|
||||
timestamp, versionInfo.UpdatedBy,
|
||||
updatedAt, versionInfo.UpdatedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -411,27 +450,35 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
"body=?, size=?, etag=?, version=?, "+
|
||||
"updated_at=?, updated_by=?,"+
|
||||
"name=?, description=?,"+
|
||||
"labels=?, fields=?, errors=? "+
|
||||
"labels=?, fields=?, errors=?, "+
|
||||
"origin=?, origin_ts=? "+
|
||||
"WHERE path=?",
|
||||
body, versionInfo.Size, etag, versionInfo.Version,
|
||||
timestamp, versionInfo.UpdatedBy,
|
||||
updatedAt, versionInfo.UpdatedBy,
|
||||
summary.model.Name, summary.model.Description,
|
||||
summary.labels, summary.fields, summary.errors,
|
||||
r.Origin, timestamp,
|
||||
path,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert the new row
|
||||
if createdAt < 1000 {
|
||||
createdAt = updatedAt
|
||||
}
|
||||
if createdBy == "" {
|
||||
createdBy = updatedBy
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, "INSERT INTO object ("+
|
||||
"path, parent_folder_path, kind, size, body, etag, version,"+
|
||||
"updated_at, updated_by, created_at, created_by,"+
|
||||
"name, description,"+
|
||||
"path, parent_folder_path, kind, size, body, etag, version, "+
|
||||
"updated_at, updated_by, created_at, created_by, "+
|
||||
"name, description, origin, origin_ts, "+
|
||||
"labels, fields, errors) "+
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
path, getParentFolderPath(grn.Kind, path), grn.Kind, versionInfo.Size, body, etag, versionInfo.Version,
|
||||
timestamp, versionInfo.UpdatedBy, timestamp, versionInfo.UpdatedBy, // created + updated are the same
|
||||
summary.model.Name, summary.model.Description,
|
||||
updatedAt, createdBy, createdAt, createdBy, // created + updated are the same
|
||||
summary.model.Name, summary.model.Description, r.Origin, timestamp,
|
||||
summary.labels, summary.fields, summary.errors,
|
||||
)
|
||||
return err
|
||||
@ -443,6 +490,28 @@ func (s *sqlObjectServer) Write(ctx context.Context, r *object.WriteObjectReques
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
func (s *sqlObjectServer) fillCreationInfo(ctx context.Context, tx *session.SessionTx, path string, createdAt *int64, createdBy *string) error {
|
||||
if *createdAt > 1000 {
|
||||
ignore := int64(0)
|
||||
createdAt = &ignore
|
||||
}
|
||||
if *createdBy == "" {
|
||||
ignore := ""
|
||||
createdBy = &ignore
|
||||
}
|
||||
|
||||
rows, err := tx.Query(ctx, "SELECT created_at,created_by FROM object WHERE path=?", path)
|
||||
if err == nil {
|
||||
if rows.Next() {
|
||||
err = rows.Scan(&createdAt, &createdBy)
|
||||
}
|
||||
if err == nil {
|
||||
err = rows.Close()
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sqlObjectServer) selectForUpdate(ctx context.Context, tx *session.SessionTx, path string) (*object.ObjectVersionInfo, error) {
|
||||
q := "SELECT etag,version,updated_at,size FROM object WHERE path=?"
|
||||
if false { // TODO, MYSQL/PosgreSQL can lock the row " FOR UPDATE"
|
||||
@ -462,7 +531,7 @@ func (s *sqlObjectServer) selectForUpdate(ctx context.Context, tx *session.Sessi
|
||||
return current, err
|
||||
}
|
||||
|
||||
func (s *sqlObjectServer) prepare(ctx context.Context, r *object.WriteObjectRequest) (*summarySupport, []byte, error) {
|
||||
func (s *sqlObjectServer) prepare(ctx context.Context, r *object.AdminWriteObjectRequest) (*summarySupport, []byte, error) {
|
||||
grn := r.GRN
|
||||
builder := s.kinds.GetSummaryBuilder(grn.Kind)
|
||||
if builder == nil {
|
||||
@ -490,27 +559,29 @@ func (s *sqlObjectServer) Delete(ctx context.Context, r *object.DeleteObjectRequ
|
||||
|
||||
rsp := &object.DeleteObjectResponse{}
|
||||
err = s.sess.WithTransaction(ctx, func(tx *session.SessionTx) error {
|
||||
results, err := tx.Exec(ctx, "DELETE FROM object WHERE path=?", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows > 0 {
|
||||
rsp.OK = true
|
||||
}
|
||||
|
||||
// TODO: keep history? would need current version bump, and the "write" would have to get from history
|
||||
_, _ = tx.Exec(ctx, "DELETE FROM object_history WHERE path=?", path)
|
||||
_, _ = tx.Exec(ctx, "DELETE FROM object_labels WHERE path=?", path)
|
||||
_, _ = tx.Exec(ctx, "DELETE FROM object_ref WHERE path=?", path)
|
||||
return nil
|
||||
rsp.OK, err = doDelete(ctx, tx, path)
|
||||
return err
|
||||
})
|
||||
return rsp, err
|
||||
}
|
||||
|
||||
func doDelete(ctx context.Context, tx *session.SessionTx, path string) (bool, error) {
|
||||
results, err := tx.Exec(ctx, "DELETE FROM object WHERE path=?", path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
rows, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// TODO: keep history? would need current version bump, and the "write" would have to get from history
|
||||
_, _ = tx.Exec(ctx, "DELETE FROM object_history WHERE path=?", path)
|
||||
_, _ = tx.Exec(ctx, "DELETE FROM object_labels WHERE path=?", path)
|
||||
_, _ = tx.Exec(ctx, "DELETE FROM object_ref WHERE path=?", path)
|
||||
return rows > 0, err
|
||||
}
|
||||
|
||||
func (s *sqlObjectServer) History(ctx context.Context, r *object.ObjectHistoryRequest) (*object.ObjectHistoryResponse, error) {
|
||||
route, err := s.getObjectKey(ctx, r.GRN)
|
||||
if err != nil {
|
||||
|
11
pkg/services/store/object/utils.go
Normal file
11
pkg/services/store/object/utils.go
Normal file
@ -0,0 +1,11 @@
|
||||
package object
|
||||
|
||||
// The admin request is a superset of write request features
|
||||
func ToAdminWriteObjectRequest(req *WriteObjectRequest) *AdminWriteObjectRequest {
|
||||
return &AdminWriteObjectRequest{
|
||||
GRN: req.GRN,
|
||||
Body: req.Body,
|
||||
Comment: req.Comment,
|
||||
PreviousVersion: req.PreviousVersion,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user