mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 06:22:45 +08:00
598 lines
16 KiB
Go
598 lines
16 KiB
Go
package folderimpl
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/selection"
|
|
|
|
claims "github.com/grafana/authlib/types"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
|
|
|
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
const tracePrefix = "folder.unifiedstore."
|
|
|
|
type FolderUnifiedStoreImpl struct {
|
|
log log.Logger
|
|
k8sclient client.K8sHandler
|
|
userService user.Service
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
// sqlStore implements the store interface.
|
|
var _ folder.Store = (*FolderUnifiedStoreImpl)(nil)
|
|
|
|
func ProvideUnifiedStore(k8sHandler client.K8sHandler, userService user.Service, tracer trace.Tracer) *FolderUnifiedStoreImpl {
|
|
return &FolderUnifiedStoreImpl{
|
|
k8sclient: k8sHandler,
|
|
log: log.New("folder-store"),
|
|
userService: userService,
|
|
tracer: tracer,
|
|
}
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"Create")
|
|
defer span.End()
|
|
|
|
obj, err := internalfolders.LegacyCreateCommandToUnstructured(&cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID, v1.CreateOptions{
|
|
FieldValidation: v1.FieldValidationIgnore})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
folder, err := ss.UnstructuredToLegacyFolder(ctx, out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return folder, err
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) Delete(ctx context.Context, UIDs []string, orgID int64) error {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"Delete")
|
|
defer span.End()
|
|
|
|
for _, uid := range UIDs {
|
|
err := ss.k8sclient.Delete(ctx, uid, orgID, v1.DeleteOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"Update")
|
|
defer span.End()
|
|
|
|
obj, err := ss.k8sclient.Get(ctx, cmd.UID, cmd.OrgID, v1.GetOptions{})
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, dashboards.ErrFolderNotFound
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
updated := obj.DeepCopy()
|
|
|
|
if cmd.NewTitle != nil {
|
|
err = unstructured.SetNestedField(updated.Object, *cmd.NewTitle, "spec", "title")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if cmd.NewDescription != nil {
|
|
err = unstructured.SetNestedField(updated.Object, *cmd.NewDescription, "spec", "description")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if cmd.NewParentUID != nil {
|
|
meta, err := utils.MetaAccessor(updated)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
meta.SetFolder(*cmd.NewParentUID)
|
|
} else {
|
|
// only compare versions if not moving the folder
|
|
if !cmd.Overwrite && (cmd.Version != int(obj.GetGeneration())) {
|
|
return nil, dashboards.ErrDashboardVersionMismatch
|
|
}
|
|
}
|
|
|
|
out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID, v1.UpdateOptions{
|
|
FieldValidation: v1.FieldValidationIgnore,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ss.UnstructuredToLegacyFolder(ctx, out)
|
|
}
|
|
|
|
// If WithFullpath is true it computes also the full path of a folder.
|
|
// The full path is a string that contains the titles of all parent folders separated by a slash.
|
|
// For example, if the folder structure is:
|
|
//
|
|
// A
|
|
// └── B
|
|
// └── C
|
|
//
|
|
// The full path of C is "A/B/C".
|
|
// The full path of B is "A/B".
|
|
// The full path of A is "A".
|
|
// If a folder contains a slash in its title, it is escaped with a backslash.
|
|
// For example, if the folder structure is:
|
|
//
|
|
// A
|
|
// └── B/C
|
|
//
|
|
// The full path of C is "A/B\/C".
|
|
func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"Get")
|
|
defer span.End()
|
|
|
|
out, err := ss.k8sclient.Get(ctx, *q.UID, q.OrgID, v1.GetOptions{})
|
|
if err != nil && !apierrors.IsNotFound(err) {
|
|
return nil, err
|
|
} else if err != nil || out == nil {
|
|
return nil, dashboards.ErrFolderNotFound
|
|
}
|
|
|
|
return ss.UnstructuredToLegacyFolder(ctx, out)
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"GetParents")
|
|
defer span.End()
|
|
|
|
hits := []*folder.Folder{}
|
|
|
|
parentUID := q.UID
|
|
for parentUID != "" {
|
|
folder, err := ss.Get(ctx, folder.GetFolderQuery{UID: &parentUID, OrgID: q.OrgID})
|
|
if err != nil {
|
|
if apierrors.IsForbidden(err) {
|
|
// If we get a Forbidden error when requesting the parent folder, it means the user does not have access
|
|
// to it, nor its parents. So we can stop looping
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
parentUID = folder.ParentUID
|
|
hits = append(hits, folder)
|
|
}
|
|
|
|
if len(hits) == 0 {
|
|
return hits, nil
|
|
}
|
|
|
|
return util.Reverse(hits[1:]), nil
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"GetChildren")
|
|
defer span.End()
|
|
|
|
// the general folder is saved as an empty string in the database
|
|
if q.UID == folder.GeneralFolderUID {
|
|
q.UID = ""
|
|
}
|
|
if q.Limit == 0 {
|
|
q.Limit = folderSearchLimit
|
|
}
|
|
if q.Page == 0 {
|
|
q.Page = 1
|
|
}
|
|
|
|
if q.UID != "" {
|
|
// the original get children query fails if the parent folder does not exist
|
|
// check that the parent exists first
|
|
_, err := ss.Get(ctx, folder.GetFolderQuery{UID: &q.UID, OrgID: q.OrgID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
req := &resourcepb.ResourceSearchRequest{
|
|
Options: &resourcepb.ListOptions{
|
|
Fields: []*resourcepb.Requirement{
|
|
{
|
|
Key: resource.SEARCH_FIELD_FOLDER,
|
|
Operator: string(selection.In),
|
|
Values: []string{q.UID},
|
|
},
|
|
},
|
|
},
|
|
Limit: q.Limit,
|
|
// legacy fallback search requires page, unistore requires offset,
|
|
// so set both
|
|
Offset: q.Limit * (q.Page - 1),
|
|
Page: q.Page,
|
|
}
|
|
|
|
// only filter the folder UIDs if they are provided in the query
|
|
if len(q.FolderUIDs) > 0 {
|
|
req.Options.Fields = append(req.Options.Fields, &resourcepb.Requirement{
|
|
Key: resource.SEARCH_FIELD_NAME,
|
|
Operator: string(selection.In),
|
|
Values: q.FolderUIDs,
|
|
})
|
|
}
|
|
|
|
// now, get children of the parent folder
|
|
out, err := ss.k8sclient.Search(ctx, q.OrgID, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := dashboardsearch.ParseResults(out, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allowK6Folder := (q.SignedInUser != nil && q.SignedInUser.IsIdentityType(claims.TypeServiceAccount))
|
|
hits := make([]*folder.FolderReference, 0)
|
|
for _, item := range res.Hits {
|
|
// filter out k6 folders if request is not from a service account
|
|
if item.Name == accesscontrol.K6FolderUID && !allowK6Folder {
|
|
continue
|
|
}
|
|
|
|
f := &folder.FolderReference{
|
|
ID: item.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID),
|
|
UID: item.Name,
|
|
Title: item.Title,
|
|
ParentUID: item.Folder,
|
|
}
|
|
|
|
if item.Field.GetNestedString(resource.SEARCH_FIELD_MANAGER_KIND) != "" {
|
|
f.ManagedBy = utils.ParseManagerKindString(item.Field.GetNestedString(resource.SEARCH_FIELD_MANAGER_KIND))
|
|
}
|
|
|
|
hits = append(hits, f)
|
|
}
|
|
|
|
return hits, nil
|
|
}
|
|
|
|
// TODO use a single query to get the height of a folder
|
|
func (ss *FolderUnifiedStoreImpl) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"GetHeight")
|
|
defer span.End()
|
|
|
|
height := -1
|
|
queue := []string{foldrUID}
|
|
for len(queue) > 0 && height <= folder.MaxNestedFolderDepth {
|
|
length := len(queue)
|
|
height++
|
|
for i := 0; i < length; i++ {
|
|
ele := queue[0]
|
|
queue = queue[1:]
|
|
if parentUID != nil && *parentUID == ele {
|
|
return 0, folder.ErrCircularReference
|
|
}
|
|
folders, err := ss.GetChildren(ctx, folder.GetChildrenQuery{UID: ele, OrgID: orgID})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for _, f := range folders {
|
|
queue = append(queue, f.UID)
|
|
}
|
|
}
|
|
}
|
|
if height > folder.MaxNestedFolderDepth {
|
|
ss.log.Warn("folder height exceeds the maximum allowed depth, You might have a circular reference", "uid", foldrUID, "orgId", orgID, "maxDepth", folder.MaxNestedFolderDepth)
|
|
}
|
|
return height, nil
|
|
}
|
|
|
|
// GetFolders returns org folders by their UIDs.
|
|
// If UIDs is empty, it returns all folders in the org.
|
|
// If WithFullpath is true it computes also the full path of a folder.
|
|
// The full path is a string that contains the titles of all parent folders separated by a slash.
|
|
// For example, if the folder structure is:
|
|
//
|
|
// A
|
|
// └── B
|
|
// └── C
|
|
//
|
|
// The full path of C is "A/B/C".
|
|
// The full path of B is "A/B".
|
|
// The full path of A is "A".
|
|
// If a folder contains a slash in its title, it is escaped with a backslash.
|
|
// For example, if the folder structure is:
|
|
//
|
|
// A
|
|
// └── B/C
|
|
//
|
|
// The full path of C is "A/B\/C".
|
|
//
|
|
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
|
|
// For example, if the folder structure is:
|
|
//
|
|
// A (uid: "uid1")
|
|
// └── B (uid: "uid2")
|
|
// └── C (uid: "uid3")
|
|
//
|
|
// The full path UIDs of C is "uid1/uid2/uid3".
|
|
// The full path UIDs of B is "uid1/uid2".
|
|
// The full path UIDs of A is "uid1".
|
|
func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFoldersFromStoreQuery) ([]*folder.Folder, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"GetFolders")
|
|
defer span.End()
|
|
|
|
opts := v1.ListOptions{}
|
|
if q.WithFullpath || q.WithFullpathUIDs {
|
|
// only supported in modes 0-2, to keep the alerting queries from causing tons of get folder requests
|
|
// to retrieve the parent for all folders in grafana
|
|
opts.LabelSelector = utils.LabelGetFullpath + "=true"
|
|
}
|
|
|
|
out, err := ss.list(ctx, q.OrgID, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// convert item to legacy folder format
|
|
folders, err := ss.UnstructuredToLegacyFolderList(ctx, out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filters := make(map[string]struct{}, len(q.UIDs))
|
|
for _, uid := range q.UIDs {
|
|
filters[uid] = struct{}{}
|
|
}
|
|
|
|
folderMap := make(map[string]*folder.Folder)
|
|
relations := make(map[string]string)
|
|
if q.WithFullpath || q.WithFullpathUIDs {
|
|
for _, folder := range folders {
|
|
folderMap[folder.UID] = folder
|
|
relations[folder.UID] = folder.ParentUID
|
|
}
|
|
}
|
|
|
|
hits := make([]*folder.Folder, 0, len(folders))
|
|
for _, f := range folders {
|
|
if shouldSkipFolder(f, filters) {
|
|
continue
|
|
}
|
|
|
|
if (q.WithFullpath || q.WithFullpathUIDs) && f.Fullpath == "" {
|
|
buildFolderFullPaths(f, relations, folderMap)
|
|
}
|
|
|
|
hits = append(hits, f)
|
|
}
|
|
|
|
// TODO: return all nodes under those ancestors, requires building a tree
|
|
// if len(q.AncestorUIDs) > 0 {
|
|
// }
|
|
|
|
return hits, nil
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"GetDescendants")
|
|
defer span.End()
|
|
|
|
out, err := ss.list(ctx, orgID, v1.ListOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// convert item to legacy folder format
|
|
folders, err := ss.UnstructuredToLegacyFolderList(ctx, out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nodes := map[string]*folder.Folder{}
|
|
for _, f := range folders {
|
|
nodes[f.UID] = f
|
|
}
|
|
|
|
tree := map[string]map[string]*folder.Folder{}
|
|
|
|
for uid, f := range nodes {
|
|
parentUID := f.ParentUID
|
|
if parentUID == "" {
|
|
parentUID = "general"
|
|
}
|
|
|
|
if tree[parentUID] == nil {
|
|
tree[parentUID] = map[string]*folder.Folder{}
|
|
}
|
|
|
|
tree[parentUID][uid] = f
|
|
}
|
|
|
|
descendantsMap := map[string]*folder.Folder{}
|
|
getDescendants(nodes, tree, ancestor_uid, descendantsMap)
|
|
|
|
descendants := []*folder.Folder{}
|
|
for _, f := range descendantsMap {
|
|
descendants = append(descendants, f)
|
|
}
|
|
|
|
return descendants, nil
|
|
}
|
|
|
|
func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string]*folder.Folder, ancestor_uid string, descendantsMap map[string]*folder.Folder) {
|
|
for uid := range tree[ancestor_uid] {
|
|
descendantsMap[uid] = nodes[uid]
|
|
getDescendants(nodes, tree, uid, descendantsMap)
|
|
}
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) CountFolderContent(ctx context.Context, orgID int64, ancestor_uid string) (folder.DescendantCounts, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"CountFolderContent")
|
|
defer span.End()
|
|
|
|
counts, err := ss.k8sclient.Get(ctx, ancestor_uid, orgID, v1.GetOptions{}, "counts")
|
|
if err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
return nil, dashboards.ErrFolderNotFound
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
res, err := toFolderLegacyCounts(counts)
|
|
return *res, err
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) CountInOrg(ctx context.Context, orgID int64) (int64, error) {
|
|
resp, err := ss.k8sclient.GetStats(ctx, orgID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if len(resp.Stats) != 1 {
|
|
return 0, fmt.Errorf("expected 1 stat, got %d", len(resp.Stats))
|
|
}
|
|
|
|
return resp.Stats[0].Count, nil
|
|
}
|
|
|
|
func (ss *FolderUnifiedStoreImpl) list(ctx context.Context, orgID int64, opts v1.ListOptions) (*unstructured.UnstructuredList, error) {
|
|
ctx, span := ss.tracer.Start(ctx, tracePrefix+"list")
|
|
defer span.End()
|
|
|
|
var allItems []unstructured.Unstructured
|
|
|
|
listOpts := opts.DeepCopy()
|
|
|
|
if listOpts.Limit == 0 {
|
|
listOpts.Limit = folderListLimit
|
|
}
|
|
|
|
for {
|
|
out, err := ss.k8sclient.List(ctx, orgID, *listOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if out == nil {
|
|
return nil, fmt.Errorf("k8s folder list returned nil")
|
|
}
|
|
|
|
if len(out.Items) > 0 {
|
|
allItems = append(allItems, out.Items...)
|
|
}
|
|
|
|
if out.GetContinue() == "" || (opts.Limit > 0 && int64(len(allItems)) >= opts.Limit) {
|
|
break
|
|
}
|
|
|
|
listOpts.Continue = out.GetContinue()
|
|
}
|
|
|
|
result := &unstructured.UnstructuredList{
|
|
Items: allItems,
|
|
}
|
|
|
|
if opts.Limit > 0 && int64(len(allItems)) > opts.Limit {
|
|
result.Items = allItems[:opts.Limit]
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) {
|
|
ds, err := folderv1.UnstructuredToDescendantCounts(u)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var out = make(folder.DescendantCounts)
|
|
for _, v := range ds.Counts {
|
|
// if stats come from unified storage, we will use them
|
|
if v.Group != "sql-fallback" {
|
|
out[v.Resource] = v.Count
|
|
continue
|
|
}
|
|
// if stats are from single tenant DB and they are not in unified storage, we will use them
|
|
if _, ok := out[v.Resource]; !ok {
|
|
out[v.Resource] = v.Count
|
|
}
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
func computeFullPath(parents []*folder.Folder) (string, string) {
|
|
fullpath := make([]string, len(parents))
|
|
fullpathUIDs := make([]string, len(parents))
|
|
for i, p := range parents {
|
|
fullpath[i] = p.Title
|
|
fullpathUIDs[i] = p.UID
|
|
}
|
|
return strings.Join(fullpath, "/"), strings.Join(fullpathUIDs, "/")
|
|
}
|
|
|
|
func buildFolderFullPaths(f *folder.Folder, relations map[string]string, folderMap map[string]*folder.Folder) {
|
|
titles := make([]string, 0)
|
|
uids := make([]string, 0)
|
|
|
|
titles = append(titles, f.Title)
|
|
uids = append(uids, f.UID)
|
|
|
|
currentUID := f.UID
|
|
for currentUID != "" {
|
|
parentUID, exists := relations[currentUID]
|
|
if !exists {
|
|
break
|
|
}
|
|
|
|
if parentUID == "" {
|
|
break
|
|
}
|
|
|
|
parentFolder, exists := folderMap[parentUID]
|
|
if !exists {
|
|
break
|
|
}
|
|
titles = append(titles, parentFolder.Title)
|
|
uids = append(uids, parentFolder.UID)
|
|
currentUID = parentFolder.UID
|
|
}
|
|
|
|
f.Fullpath = strings.Join(util.Reverse(titles), "/")
|
|
f.FullpathUIDs = strings.Join(util.Reverse(uids), "/")
|
|
}
|
|
|
|
func shouldSkipFolder(f *folder.Folder, filterUIDs map[string]struct{}) bool {
|
|
if len(filterUIDs) == 0 {
|
|
return false
|
|
}
|
|
_, exists := filterUIDs[f.UID]
|
|
return !exists
|
|
}
|