EntityAPI: Include folder support and watch API stubs (#61338)

This commit is contained in:
Ryan McKinley
2023-01-13 00:39:36 +00:00
committed by GitHub
parent da3f02d81b
commit 0c20fe0ac9
10 changed files with 996 additions and 346 deletions
@@ -0,0 +1,122 @@
package sqlstash
import (
"context"
"encoding/json"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/services/store/entity"
)
type folderInfo struct {
UID string `json:"uid"`
Name string `json:"name"`
Slug string `json:"slug"`
// Build the tree
ParentUID string `json:"-"`
// Added after query
children []*folderInfo
parent *folderInfo
}
// This will replace all entries in `entity_folder`
// This is pretty heavy weight, but it does give us a sorted folder list
// NOTE: this could be done async with a mutex/lock? reconciler pattern
func updateFolderTree(ctx context.Context, tx *session.SessionTx, tenant int64) error {
_, err := tx.Exec(ctx, "DELETE FROM entity_folder WHERE tenant_id=?", tenant)
if err != nil {
return err
}
all := []*folderInfo{}
lookup := make(map[string]*folderInfo)
rows, err := tx.Query(ctx, "SELECT uid,folder,name,slug FROM entity WHERE kind=? AND tenant_id=? ORDER BY slug asc;",
models.StandardKindFolder, tenant)
if err != nil {
return err
}
for rows.Next() {
folder := folderInfo{
children: []*folderInfo{},
}
err = rows.Scan(&folder.UID, &folder.ParentUID, &folder.Name, &folder.Slug)
if err != nil {
return err
}
lookup[folder.UID] = &folder
all = append(all, &folder)
}
err = rows.Close()
if err != nil {
return err
}
root := &folderInfo{
Name: "Root",
children: []*folderInfo{},
}
lookup[""] = root
lost := []*folderInfo{}
// already sorted by slug
for _, folder := range all {
parent, ok := lookup[folder.ParentUID]
if ok {
folder.parent = parent
parent.children = append(parent.children, folder)
} else {
lost = append(lost, folder)
}
}
for _, folder := range root.children {
err = addFolderInfo(ctx, tx, tenant, []*folderInfo{folder}, false)
if err != nil {
return err
}
}
for _, folder := range lost {
err = addFolderInfo(ctx, tx, tenant, []*folderInfo{folder}, true)
if err != nil {
return err
}
}
return err
}
func addFolderInfo(ctx context.Context, tx *session.SessionTx, tenant int64, tree []*folderInfo, isDetached bool) error {
folder := tree[len(tree)-1] // last item in the tree
js, _ := json.Marshal(tree)
slugPath := "/"
for _, f := range tree {
slugPath += f.Slug + "/"
}
grn := entity.GRN{TenantId: tenant, Kind: models.StandardKindFolder, UID: folder.UID}
_, err := tx.Exec(ctx,
`INSERT INTO entity_folder `+
"(grn, tenant_id, uid, slug_path, tree, depth, detached) "+
`VALUES (?, ?, ?, ?, ?, ?, ?)`,
grn.ToGRNString(),
tenant,
folder.UID,
slugPath,
string(js),
len(tree),
isDetached,
)
if err != nil {
return err
}
for _, sub := range folder.children {
err := addFolderInfo(ctx, tx, tenant, append(tree, sub), isDetached)
if err != nil {
return err
}
}
return nil
}
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/grpcserver"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/services/store"
@@ -46,7 +47,7 @@ type sqlEntityServer struct {
func getReadSelect(r *entity.ReadEntityRequest) string {
fields := []string{
"tenant_id", "kind", "uid", // The PK
"tenant_id", "kind", "uid", "folder", // GRN + folder
"version", "size", "etag", "errors", // errors are always returned
"created_at", "created_by",
"updated_at", "updated_by",
@@ -56,7 +57,7 @@ func getReadSelect(r *entity.ReadEntityRequest) string {
fields = append(fields, `body`)
}
if r.WithSummary {
fields = append(fields, "name", "slug", "folder", "description", "labels", "fields")
fields = append(fields, "name", "slug", "description", "labels", "fields")
}
return "SELECT " + strings.Join(fields, ",") + " FROM entity WHERE "
}
@@ -69,7 +70,7 @@ func (s *sqlEntityServer) rowToReadEntityResponse(ctx context.Context, rows *sql
summaryjson := &summarySupport{}
args := []interface{}{
&raw.GRN.TenantId, &raw.GRN.Kind, &raw.GRN.UID,
&raw.GRN.TenantId, &raw.GRN.Kind, &raw.GRN.UID, &raw.Folder,
&raw.Version, &raw.Size, &raw.ETag, &summaryjson.errors,
&raw.CreatedAt, &raw.CreatedBy,
&raw.UpdatedAt, &raw.UpdatedBy,
@@ -79,7 +80,7 @@ func (s *sqlEntityServer) rowToReadEntityResponse(ctx context.Context, rows *sql
args = append(args, &raw.Body)
}
if r.WithSummary {
args = append(args, &summaryjson.name, &summaryjson.slug, &summaryjson.folder, &summaryjson.description, &summaryjson.labels, &summaryjson.fields)
args = append(args, &summaryjson.name, &summaryjson.slug, &summaryjson.description, &summaryjson.labels, &summaryjson.fields)
}
err := rows.Scan(args...)
@@ -303,6 +304,7 @@ func (s *sqlEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEn
return nil, err
}
isFolder := models.StandardKindFolder == r.GRN.Kind
etag := createContentsHash(body)
rsp := &entity.WriteEntityResponse{
GRN: grn,
@@ -324,7 +326,7 @@ func (s *sqlEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEn
return err
}
}
_, err = doDelete(ctx, tx, oid)
_, err = doDelete(ctx, tx, grn)
if err != nil {
return err
}
@@ -445,6 +447,10 @@ func (s *sqlEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEn
origin.Source, origin.Key, timestamp,
oid,
)
if isFolder && err == nil {
err = updateFolderTree(ctx, tx, grn.TenantId)
}
return err
}
@@ -475,6 +481,9 @@ func (s *sqlEntityServer) AdminWrite(ctx context.Context, r *entity.AdminWriteEn
summary.labels, summary.fields, summary.errors,
origin.Source, origin.Key, origin.Time,
)
if isFolder && err == nil {
err = updateFolderTree(ctx, tx, grn.TenantId)
}
return err
})
rsp.SummaryJson = summary.marshaled
@@ -537,11 +546,6 @@ func (s *sqlEntityServer) prepare(ctx context.Context, r *entity.AdminWriteEntit
return nil, nil, err
}
summaryjson, err := newSummarySupport(summary)
if err != nil {
return nil, nil, err
}
// Update a summary based on the name (unless the root suggested one)
if summary.Slug == "" {
t := summary.Name
@@ -551,6 +555,11 @@ func (s *sqlEntityServer) prepare(ctx context.Context, r *entity.AdminWriteEntit
summary.Slug = slugify.Slugify(t)
}
summaryjson, err := newSummarySupport(summary)
if err != nil {
return nil, nil, err
}
return summaryjson, body, nil
}
@@ -562,14 +571,15 @@ func (s *sqlEntityServer) Delete(ctx context.Context, r *entity.DeleteEntityRequ
rsp := &entity.DeleteEntityResponse{}
err = s.sess.WithTransaction(ctx, func(tx *session.SessionTx) error {
rsp.OK, err = doDelete(ctx, tx, grn.ToGRNString())
rsp.OK, err = doDelete(ctx, tx, grn)
return err
})
return rsp, err
}
func doDelete(ctx context.Context, tx *session.SessionTx, grn string) (bool, error) {
results, err := tx.Exec(ctx, "DELETE FROM entity WHERE grn=?", grn)
func doDelete(ctx context.Context, tx *session.SessionTx, grn *entity.GRN) (bool, error) {
str := grn.ToGRNString()
results, err := tx.Exec(ctx, "DELETE FROM entity WHERE grn=?", str)
if err != nil {
return false, err
}
@@ -579,9 +589,14 @@ func doDelete(ctx context.Context, tx *session.SessionTx, grn string) (bool, err
}
// TODO: keep history? would need current version bump, and the "write" would have to get from history
_, _ = tx.Exec(ctx, "DELETE FROM entity_history WHERE grn=?", grn)
_, _ = tx.Exec(ctx, "DELETE FROM entity_labels WHERE grn=?", grn)
_, _ = tx.Exec(ctx, "DELETE FROM entity_ref WHERE grn=?", grn)
_, _ = tx.Exec(ctx, "DELETE FROM entity_history WHERE grn=?", str)
_, _ = tx.Exec(ctx, "DELETE FROM entity_labels WHERE grn=?", str)
_, _ = tx.Exec(ctx, "DELETE FROM entity_ref WHERE grn=?", str)
if grn.Kind == models.StandardKindFolder {
err = updateFolderTree(ctx, tx, grn.TenantId)
}
return rows > 0, err
}
@@ -761,3 +776,7 @@ func (s *sqlEntityServer) Search(ctx context.Context, r *entity.EntitySearchRequ
return rsp, err
}
func (s *sqlEntityServer) Watch(*entity.EntityWatchRequest, entity.EntityStore_WatchServer) error {
return fmt.Errorf("unimplemented")
}
@@ -11,7 +11,6 @@ type summarySupport struct {
name string
description *string // null or empty
slug *string // null or empty
folder *string // null or empty
labels *string
fields *string
errors *string // should not allow saving with this!
@@ -37,9 +36,6 @@ func newSummarySupport(summary *models.EntitySummary) (*summarySupport, error) {
if summary.Slug != "" {
s.slug = &summary.Slug
}
if summary.Folder != "" {
s.folder = &summary.Folder
}
if len(summary.Labels) > 0 {
js, err = json.Marshal(summary.Labels)
if err != nil {
@@ -81,9 +77,6 @@ func (s summarySupport) toEntitySummary() (*models.EntitySummary, error) {
if s.slug != nil {
summary.Slug = *s.slug
}
if s.folder != nil {
summary.Folder = *s.folder
}
if s.labels != nil {
b := []byte(*s.labels)
err = json.Unmarshal(b, &summary.Labels)