grafana/pkg/registry/apis/folders/validate.go

154 lines
3.7 KiB
Go

package folders
import (
"context"
"fmt"
"slices"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/registry/rest"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/util"
)
func validateOnCreate(ctx context.Context, f *folders.Folder, getter parentsGetter, maxDepth int) error {
id := f.Name
if slices.Contains([]string{
folder.GeneralFolderUID,
folder.SharedWithMeFolderUID,
}, id) {
return dashboards.ErrFolderInvalidUID
}
meta, err := utils.MetaAccessor(f)
if err != nil {
return fmt.Errorf("unable to read metadata from object: %w", err)
}
if !util.IsValidShortUID(id) {
return dashboards.ErrDashboardInvalidUid
}
if util.IsShortUIDTooLong(id) {
return dashboards.ErrDashboardUidTooLong
}
if f.Spec.Title == "" {
return dashboards.ErrFolderTitleEmpty
}
parentName := meta.GetFolder()
if parentName == "" {
return nil // OK, we do not need to validate the tree
}
if parentName == f.Name {
return folder.ErrFolderCannotBeParentOfItself
}
parents, err := getter(ctx, f)
if err != nil {
return fmt.Errorf("unable to create folder inside parent: %w", err)
}
// Can not create a folder that will be too deep.
// We need to add +1 as we also have the root folder as part of the parents.
if len(parents.Items) > maxDepth+1 {
return fmt.Errorf("folder max depth exceeded, max depth is %d", maxDepth)
}
return nil
}
func validateOnUpdate(ctx context.Context,
obj *folders.Folder,
old *folders.Folder,
getter rest.Getter,
parents parentsGetter,
maxDepth int,
) error {
folderObj, err := utils.MetaAccessor(obj)
if err != nil {
return err
}
oldFolder, err := utils.MetaAccessor(old)
if err != nil {
return err
}
if obj.Spec.Title == "" {
return dashboards.ErrFolderTitleEmpty
}
if folderObj.GetFolder() == oldFolder.GetFolder() {
return nil
}
// Validate the move operation
newParent := folderObj.GetFolder()
// If we move to root, we don't need to validate the depth.
if newParent == folder.RootFolderUID {
return nil
}
// folder cannot be moved to a k6 folder
if newParent == accesscontrol.K6FolderUID {
return fmt.Errorf("k6 project may not be moved")
}
parentObj, err := getter.Get(ctx, newParent, &metav1.GetOptions{})
if err != nil {
return fmt.Errorf("move target not found %w", err)
}
parent, ok := parentObj.(*folders.Folder)
if !ok {
return fmt.Errorf("expected folder, found %T", parentObj)
}
//FIXME: until we have a way to represent the tree, we can only
// look at folder parents to check how deep the new folder tree will be
info, err := parents(ctx, parent)
if err != nil {
return err
}
// if by moving a folder we exceed the max depth, return an error
if len(info.Items)+1 >= maxDepth {
return folder.ErrMaximumDepthReached.Errorf("maximum folder depth reached")
}
return nil
}
func validateOnDelete(ctx context.Context,
f *folders.Folder,
searcher resourcepb.ResourceIndexClient,
) error {
resp, err := searcher.GetStats(ctx, &resourcepb.ResourceStatsRequest{Namespace: f.Namespace, Folder: f.Name})
if err != nil {
return err
}
if resp != nil && resp.Error != nil {
return fmt.Errorf("could not verify if folder is empty: %v", resp.Error)
}
if resp.Stats == nil {
return fmt.Errorf("could not verify if folder is empty: %v", resp.Error)
}
for _, v := range resp.Stats {
if v.Count > 0 {
return folder.ErrFolderNotEmpty.Errorf("folder is not empty, contains %d resources", v.Count)
}
}
return nil
}