Storage: add an admin write flavor that can explicitly set the user/time (#58618)

pull/59063/head
Ryan McKinley 3 years ago committed by GitHub
parent 69b5a9c752
commit 5934407443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 44
      pkg/services/export/object_store.go
  2. 8
      pkg/services/sqlstore/migrations/object_store_mig.go
  3. 17
      pkg/services/store/object/dummy/dummy_server.go
  4. 8
      pkg/services/store/object/dummy/fake_store.go
  5. 12
      pkg/services/store/object/json.go
  6. 750
      pkg/services/store/object/object.pb.go
  7. 61
      pkg/services/store/object/object.proto
  8. 122
      pkg/services/store/object/object_grpc.pb.go
  9. 179
      pkg/services/store/object/sqlstash/sql_storage_server.go
  10. 11
      pkg/services/store/object/utils.go

@ -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 {

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