EntityStore: Move slug+folder to summary metadata (#59620)

This commit is contained in:
Ryan McKinley
2022-12-01 14:45:08 -05:00
committed by GitHub
parent 6dbe3b555f
commit fb98a97efa
13 changed files with 468 additions and 1074 deletions
@@ -1,398 +0,0 @@
package dummy
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/x/persistentcollection"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/kind"
"github.com/grafana/grafana/pkg/setting"
)
type EntityVersionWithBody struct {
*entity.EntityVersionInfo `json:"info,omitempty"`
Body []byte `json:"body,omitempty"`
}
type EntityWithHistory struct {
Entity *entity.Entity `json:"entity,omitempty"`
History []*EntityVersionWithBody `json:"history,omitempty"`
}
var (
// increment when Entity changes
rawEntityVersion = 10
)
// Make sure we implement both store + admin
var _ entity.EntityStoreServer = &dummyEntityServer{}
var _ entity.EntityStoreAdminServer = &dummyEntityServer{}
func ProvideDummyEntityServer(cfg *setting.Cfg, grpcServerProvider grpcserver.Provider, kinds kind.KindRegistry) entity.EntityStoreServer {
objectServer := &dummyEntityServer{
collection: persistentcollection.NewLocalFSPersistentCollection[*EntityWithHistory]("raw-object", cfg.DataPath, rawEntityVersion),
log: log.New("in-memory-object-server"),
kinds: kinds,
}
entity.RegisterEntityStoreServer(grpcServerProvider.GetServer(), objectServer)
return objectServer
}
type dummyEntityServer struct {
log log.Logger
collection persistentcollection.PersistentCollection[*EntityWithHistory]
kinds kind.KindRegistry
}
func namespaceFromUID(grn *entity.GRN) string {
// TODO
return "orgId-1"
}
func (i *dummyEntityServer) findEntity(ctx context.Context, grn *entity.GRN, version string) (*EntityWithHistory, *entity.Entity, error) {
if grn == nil {
return nil, nil, errors.New("GRN must not be nil")
}
obj, err := i.collection.FindFirst(ctx, namespaceFromUID(grn), func(i *EntityWithHistory) (bool, error) {
return grn.Equals(i.Entity.GRN), nil
})
if err != nil {
return nil, nil, err
}
if obj == nil {
return nil, nil, nil
}
getLatestVersion := version == ""
if getLatestVersion {
return obj, obj.Entity, nil
}
for _, objVersion := range obj.History {
if objVersion.Version == version {
copy := &entity.Entity{
GRN: obj.Entity.GRN,
CreatedAt: obj.Entity.CreatedAt,
CreatedBy: obj.Entity.CreatedBy,
UpdatedAt: objVersion.UpdatedAt,
UpdatedBy: objVersion.UpdatedBy,
ETag: objVersion.ETag,
Version: objVersion.Version,
// Body is added from the dummy server cache (it does not exist in EntityVersionInfo)
Body: objVersion.Body,
}
return obj, copy, nil
}
}
return obj, nil, nil
}
func (i *dummyEntityServer) Read(ctx context.Context, r *entity.ReadEntityRequest) (*entity.ReadEntityResponse, error) {
grn := getFullGRN(ctx, r.GRN)
_, objVersion, err := i.findEntity(ctx, grn, r.Version)
if err != nil {
return nil, err
}
if objVersion == nil {
return &entity.ReadEntityResponse{
Entity: nil,
SummaryJson: nil,
}, nil
}
rsp := &entity.ReadEntityResponse{
Entity: objVersion,
}
if r.WithSummary {
// Since we do not store the summary, we can just recreate on demand
builder := i.kinds.GetSummaryBuilder(r.GRN.Kind)
if builder != nil {
summary, _, e2 := builder(ctx, r.GRN.UID, objVersion.Body)
if e2 != nil {
return nil, e2
}
rsp.SummaryJson, err = json.Marshal(summary)
}
}
return rsp, err
}
func (i *dummyEntityServer) BatchRead(ctx context.Context, batchR *entity.BatchReadEntityRequest) (*entity.BatchReadEntityResponse, error) {
results := make([]*entity.ReadEntityResponse, 0)
for _, r := range batchR.Batch {
resp, err := i.Read(ctx, r)
if err != nil {
return nil, err
}
results = append(results, resp)
}
return &entity.BatchReadEntityResponse{Results: results}, nil
}
func createContentsHash(contents []byte) string {
hash := md5.Sum(contents)
return hex.EncodeToString(hash[:])
}
func (i *dummyEntityServer) update(ctx context.Context, r *entity.AdminWriteEntityRequest, namespace string) (*entity.WriteEntityResponse, error) {
builder := i.kinds.GetSummaryBuilder(r.GRN.Kind)
if builder == nil {
return nil, fmt.Errorf("unsupported kind: " + r.GRN.Kind)
}
rsp := &entity.WriteEntityResponse{}
updatedCount, err := i.collection.Update(ctx, namespace, func(i *EntityWithHistory) (bool, *EntityWithHistory, error) {
if !r.GRN.Equals(i.Entity.GRN) {
return false, nil, nil
}
if r.PreviousVersion != "" && i.Entity.Version != r.PreviousVersion {
return false, nil, fmt.Errorf("expected the previous version to be %s, but was %s", r.PreviousVersion, i.Entity.Version)
}
prevVersion, err := strconv.Atoi(i.Entity.Version)
if err != nil {
return false, nil, err
}
modifier := store.UserFromContext(ctx)
updated := &entity.Entity{
GRN: r.GRN,
CreatedAt: i.Entity.CreatedAt,
CreatedBy: i.Entity.CreatedBy,
UpdatedAt: time.Now().UnixMilli(),
UpdatedBy: store.GetUserIDString(modifier),
Size: int64(len(r.Body)),
ETag: createContentsHash(r.Body),
Body: r.Body,
Version: fmt.Sprintf("%d", prevVersion+1),
}
versionInfo := &EntityVersionWithBody{
Body: r.Body,
EntityVersionInfo: &entity.EntityVersionInfo{
Version: updated.Version,
UpdatedAt: updated.UpdatedAt,
UpdatedBy: updated.UpdatedBy,
Size: updated.Size,
ETag: updated.ETag,
Comment: r.Comment,
},
}
rsp.Entity = versionInfo.EntityVersionInfo
rsp.Status = entity.WriteEntityResponse_UPDATED
// When saving, it must be different than the head version
if i.Entity.ETag == updated.ETag {
versionInfo.EntityVersionInfo.Version = i.Entity.Version
rsp.Status = entity.WriteEntityResponse_UNCHANGED
return false, nil, nil
}
return true, &EntityWithHistory{
Entity: updated,
History: append(i.History, versionInfo),
}, nil
})
if err != nil {
return nil, err
}
if updatedCount == 0 && rsp.Entity == nil {
return nil, fmt.Errorf("could not find object: %v", r.GRN)
}
return rsp, nil
}
func (i *dummyEntityServer) insert(ctx context.Context, r *entity.AdminWriteEntityRequest, namespace string) (*entity.WriteEntityResponse, error) {
modifier := store.GetUserIDString(store.UserFromContext(ctx))
rawObj := &entity.Entity{
GRN: r.GRN,
UpdatedAt: time.Now().UnixMilli(),
CreatedAt: time.Now().UnixMilli(),
CreatedBy: modifier,
UpdatedBy: modifier,
Size: int64(len(r.Body)),
ETag: createContentsHash(r.Body),
Body: r.Body,
Version: fmt.Sprintf("%d", 1),
}
info := &entity.EntityVersionInfo{
Version: rawObj.Version,
UpdatedAt: rawObj.UpdatedAt,
UpdatedBy: rawObj.UpdatedBy,
Size: rawObj.Size,
ETag: rawObj.ETag,
Comment: r.Comment,
}
newObj := &EntityWithHistory{
Entity: rawObj,
History: []*EntityVersionWithBody{{
EntityVersionInfo: info,
Body: r.Body,
}},
}
err := i.collection.Insert(ctx, namespace, newObj)
if err != nil {
return nil, err
}
return &entity.WriteEntityResponse{
Error: nil,
Entity: info,
Status: entity.WriteEntityResponse_CREATED,
}, nil
}
func (i *dummyEntityServer) Write(ctx context.Context, r *entity.WriteEntityRequest) (*entity.WriteEntityResponse, error) {
return i.doWrite(ctx, entity.ToAdminWriteEntityRequest(r))
}
func (i *dummyEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEntityRequest) (*entity.WriteEntityResponse, error) {
// Check permissions?
return i.doWrite(ctx, r)
}
func (i *dummyEntityServer) doWrite(ctx context.Context, r *entity.AdminWriteEntityRequest) (*entity.WriteEntityResponse, error) {
grn := getFullGRN(ctx, r.GRN)
namespace := namespaceFromUID(grn)
obj, err := i.collection.FindFirst(ctx, namespace, func(i *EntityWithHistory) (bool, error) {
if i == nil || r == nil {
return false, nil
}
return grn.Equals(i.Entity.GRN), nil
})
if err != nil {
return nil, err
}
if obj == nil {
return i.insert(ctx, r, namespace)
}
return i.update(ctx, r, namespace)
}
func (i *dummyEntityServer) Delete(ctx context.Context, r *entity.DeleteEntityRequest) (*entity.DeleteEntityResponse, error) {
grn := getFullGRN(ctx, r.GRN)
_, err := i.collection.Delete(ctx, namespaceFromUID(grn), func(i *EntityWithHistory) (bool, error) {
if grn.Equals(i.Entity.GRN) {
if r.PreviousVersion != "" && i.Entity.Version != r.PreviousVersion {
return false, fmt.Errorf("expected the previous version to be %s, but was %s", r.PreviousVersion, i.Entity.Version)
}
return true, nil
}
return false, nil
})
if err != nil {
return nil, err
}
return &entity.DeleteEntityResponse{
OK: true,
}, nil
}
func (i *dummyEntityServer) History(ctx context.Context, r *entity.EntityHistoryRequest) (*entity.EntityHistoryResponse, error) {
grn := getFullGRN(ctx, r.GRN)
obj, _, err := i.findEntity(ctx, grn, "")
if err != nil {
return nil, err
}
rsp := &entity.EntityHistoryResponse{}
if obj != nil {
// Return the most recent versions first
// Better? save them in this order?
for i := len(obj.History) - 1; i >= 0; i-- {
rsp.Versions = append(rsp.Versions, obj.History[i].EntityVersionInfo)
}
}
return rsp, nil
}
func (i *dummyEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequest) (*entity.EntitySearchResponse, error) {
var kindMap map[string]bool
if len(r.Kind) != 0 {
kindMap = make(map[string]bool)
for _, k := range r.Kind {
kindMap[k] = true
}
}
// TODO more filters
objects, err := i.collection.Find(ctx, namespaceFromUID(&entity.GRN{}), func(i *EntityWithHistory) (bool, error) {
if len(r.Kind) != 0 {
if _, ok := kindMap[i.Entity.GRN.Kind]; !ok {
return false, nil
}
}
return true, nil
})
if err != nil {
return nil, err
}
searchResults := make([]*entity.EntitySearchResult, 0)
for _, o := range objects {
builder := i.kinds.GetSummaryBuilder(o.Entity.GRN.Kind)
if builder == nil {
continue
}
summary, clean, e2 := builder(ctx, o.Entity.GRN.UID, o.Entity.Body)
if e2 != nil {
continue
}
searchResults = append(searchResults, &entity.EntitySearchResult{
GRN: o.Entity.GRN,
Version: o.Entity.Version,
UpdatedAt: o.Entity.UpdatedAt,
UpdatedBy: o.Entity.UpdatedBy,
Name: summary.Name,
Description: summary.Description,
Body: clean,
})
}
return &entity.EntitySearchResponse{
Results: searchResults,
}, nil
}
// This sets the TenantId on the request GRN
func getFullGRN(ctx context.Context, grn *entity.GRN) *entity.GRN {
if grn.TenantId == 0 {
modifier := store.UserFromContext(ctx)
grn.TenantId = modifier.OrgID
}
return grn
}
@@ -1,89 +0,0 @@
package dummy
import (
"encoding/json"
"fmt"
"testing"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/stretchr/testify/require"
)
func TestRawEncoders(t *testing.T) {
body, err := json.Marshal(map[string]interface{}{
"hello": "world",
"field": 1.23,
})
require.NoError(t, err)
raw := &EntityVersionWithBody{
&entity.EntityVersionInfo{
Version: "A",
},
body,
}
b, err := json.Marshal(raw)
require.NoError(t, err)
str := string(b)
fmt.Printf("expect: %s", str)
require.JSONEq(t, `{"info":{"version":"A"},"body":"eyJmaWVsZCI6MS4yMywiaGVsbG8iOiJ3b3JsZCJ9"}`, str)
copy := &EntityVersionWithBody{}
err = json.Unmarshal(b, copy)
require.NoError(t, err)
}
func TestRawEntityWithHistory(t *testing.T) {
body, err := json.Marshal(map[string]interface{}{
"hello": "world",
"field": 1.23,
})
require.NoError(t, err)
raw := &EntityWithHistory{
Entity: &entity.Entity{
GRN: &entity.GRN{UID: "x"},
Version: "A",
Body: body,
},
History: make([]*EntityVersionWithBody, 0),
}
raw.History = append(raw.History, &EntityVersionWithBody{
&entity.EntityVersionInfo{
Version: "B",
},
body,
})
b, err := json.MarshalIndent(raw, "", " ")
require.NoError(t, err)
str := string(b)
//fmt.Printf("expect: %s", str)
require.JSONEq(t, `{
"entity": {
"GRN": {
"UID": "x"
},
"version": "A",
"body": {
"field": 1.23,
"hello": "world"
}
},
"history": [
{
"info": {
"version": "B"
},
"body": "eyJmaWVsZCI6MS4yMywiaGVsbG8iOiJ3b3JsZCJ9"
}
]
}`, str)
copy := &EntityVersionWithBody{}
err = json.Unmarshal(b, copy)
require.NoError(t, err)
}
@@ -25,7 +25,7 @@ func (i fakeEntityStore) Write(ctx context.Context, r *entity.WriteEntityRequest
return nil, fmt.Errorf("unimplemented")
}
func (i fakeEntityStore) Read(ctx context.Context, r *entity.ReadEntityRequest) (*entity.ReadEntityResponse, error) {
func (i fakeEntityStore) Read(ctx context.Context, r *entity.ReadEntityRequest) (*entity.Entity, error) {
return nil, fmt.Errorf("unimplemented")
}