817 lines
20 KiB
Go
817 lines
20 KiB
Go
package folders
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
|
|
)
|
|
|
|
func TestValidateCreate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
folder *folders.Folder
|
|
mockFolders map[string]*folders.Folder
|
|
expectedErr string
|
|
maxDepth int // defaults to 5 unless set
|
|
}{
|
|
{
|
|
name: "ok",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p1",
|
|
Annotations: map[string]string{"grafana.app/folder": "p2"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "some title",
|
|
},
|
|
},
|
|
mockFolders: map[string]*folders.Folder{
|
|
"p2": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p2",
|
|
Annotations: map[string]string{"grafana.app/folder": "p3"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "p2 title",
|
|
},
|
|
},
|
|
"p3": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p3",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "p3 title",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "reserved name",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "general", // can not name something with general
|
|
},
|
|
},
|
|
expectedErr: "invalid uid for folder provided",
|
|
},
|
|
{
|
|
name: "too long",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "a0123456789012345678901234567890123456789", // longer than 40
|
|
},
|
|
},
|
|
expectedErr: "uid too long, max 40 characters",
|
|
},
|
|
{
|
|
name: "bad name",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "hello world", // not a-z|0-9,
|
|
},
|
|
},
|
|
expectedErr: "uid contains illegal characters",
|
|
},
|
|
{
|
|
name: "can not be a parent of yourself",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p1",
|
|
Annotations: map[string]string{"grafana.app/folder": "p1"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "some title",
|
|
},
|
|
},
|
|
expectedErr: "folder cannot be parent of itself",
|
|
},
|
|
{
|
|
name: "can not create a tree that is too deep",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p1",
|
|
Annotations: map[string]string{"grafana.app/folder": "p2"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "some title",
|
|
},
|
|
},
|
|
mockFolders: map[string]*folders.Folder{
|
|
"p2": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p2",
|
|
Annotations: map[string]string{"grafana.app/folder": "p3"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "p2 title",
|
|
},
|
|
},
|
|
"p3": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p3",
|
|
Annotations: map[string]string{"grafana.app/folder": "p4"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "p3 title",
|
|
},
|
|
},
|
|
"p4": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "p4",
|
|
Annotations: map[string]string{"grafana.app/folder": folder.GeneralFolderUID},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "p4 title",
|
|
},
|
|
},
|
|
folder.GeneralFolderUID: {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: folder.GeneralFolderUID,
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "General",
|
|
},
|
|
},
|
|
},
|
|
maxDepth: 2,
|
|
expectedErr: "folder max depth exceeded",
|
|
},
|
|
{
|
|
name: "can create a folder in max depth",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "5",
|
|
Annotations: map[string]string{"grafana.app/folder": "4"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "some title",
|
|
},
|
|
},
|
|
mockFolders: map[string]*folders.Folder{
|
|
"4": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "4",
|
|
Annotations: map[string]string{"grafana.app/folder": "3"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "4 title",
|
|
},
|
|
},
|
|
"3": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "3",
|
|
Annotations: map[string]string{"grafana.app/folder": "2"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "3 title",
|
|
},
|
|
},
|
|
"2": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "2",
|
|
Annotations: map[string]string{"grafana.app/folder": "1"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "2 title",
|
|
},
|
|
},
|
|
"1": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "1",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "1 title",
|
|
},
|
|
},
|
|
},
|
|
maxDepth: folder.MaxNestedFolderDepth,
|
|
},
|
|
{
|
|
name: "cannot create a circular reference",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "3",
|
|
Annotations: map[string]string{"grafana.app/folder": "2"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "some title",
|
|
},
|
|
},
|
|
expectedErr: "cyclic folder references found",
|
|
mockFolders: map[string]*folders.Folder{
|
|
"2": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "2",
|
|
Annotations: map[string]string{"grafana.app/folder": "1"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "2 title",
|
|
},
|
|
},
|
|
"1": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "1",
|
|
Annotations: map[string]string{"grafana.app/folder": "3"},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "1 title",
|
|
},
|
|
},
|
|
"3": {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "3",
|
|
Annotations: map[string]string{"grafana.app/folder": folder.GeneralFolderUID},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "3 title",
|
|
},
|
|
},
|
|
folder.GeneralFolderUID: {
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: folder.GeneralFolderUID,
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "General",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
maxDepth := tt.maxDepth
|
|
if maxDepth == 0 {
|
|
maxDepth = 5
|
|
}
|
|
|
|
mockStorage := grafanarest.NewMockStorage(t)
|
|
for name, f := range tt.mockFolders {
|
|
f.Name = name
|
|
mockStorage.On("Get", context.Background(), name, &metav1.GetOptions{}).Return(f, nil).Maybe()
|
|
}
|
|
|
|
getter := newParentsGetter(mockStorage, maxDepth)
|
|
|
|
err := validateOnCreate(context.Background(), tt.folder, getter, maxDepth)
|
|
|
|
if tt.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateUpdate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
folder *folders.Folder
|
|
old *folders.Folder
|
|
parents *folders.FolderInfoList
|
|
parentsError error
|
|
allFolders []folders.Folder
|
|
expectedErr string
|
|
maxDepth int // defaults to 5 unless set
|
|
}{
|
|
{
|
|
name: "change title",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "changed",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "old title",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "error to move into k6 folder",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "k6-app",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "changed",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "old title",
|
|
},
|
|
},
|
|
expectedErr: "k6 project may not be moved",
|
|
},
|
|
{
|
|
name: "can move a folder to max depth",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "4",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "changed",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: folders.FolderSpec{
|
|
Title: "old title",
|
|
},
|
|
},
|
|
parents: &folders.FolderInfoList{
|
|
Items: []folders.FolderInfo{
|
|
{Name: "4", Parent: "3"},
|
|
{Name: "3", Parent: "2"},
|
|
{Name: "2", Parent: "1"},
|
|
{Name: "1", Parent: folder.GeneralFolderUID},
|
|
{Name: folder.GeneralFolderUID},
|
|
},
|
|
},
|
|
maxDepth: folder.MaxNestedFolderDepth,
|
|
},
|
|
{
|
|
name: "error when moving exceeds max depth",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "5",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "changed",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: folders.FolderSpec{
|
|
Title: "old title",
|
|
},
|
|
},
|
|
parents: &folders.FolderInfoList{
|
|
Items: []folders.FolderInfo{
|
|
{Name: "5", Parent: "4"},
|
|
{Name: "4", Parent: "3"},
|
|
{Name: "3", Parent: "2"},
|
|
{Name: "2", Parent: "1"},
|
|
{Name: "1", Parent: folder.GeneralFolderUID},
|
|
{Name: folder.GeneralFolderUID},
|
|
},
|
|
},
|
|
maxDepth: folder.MaxNestedFolderDepth,
|
|
expectedErr: "[folder.maximum-depth-reached]",
|
|
},
|
|
{
|
|
name: "error when moving folder under its own descendant (direct child)",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "parent",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "child",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "parent folder",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "parent",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "parent folder",
|
|
},
|
|
},
|
|
// When querying parents of "child", we get the chain: child -> parent -> root
|
|
// This means "parent" is an ancestor of "child", so we can't move "parent" under "child"
|
|
parents: &folders.FolderInfoList{
|
|
Items: []folders.FolderInfo{
|
|
{Name: "child", Parent: "parent"},
|
|
{Name: "parent", Parent: folder.GeneralFolderUID},
|
|
{Name: folder.GeneralFolderUID},
|
|
},
|
|
},
|
|
expectedErr: "cannot move folder under its own descendant",
|
|
},
|
|
{
|
|
name: "error when moving folder under its grandchild",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "grandparent",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "grandchild",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "grandparent folder",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "grandparent",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "grandparent folder",
|
|
},
|
|
},
|
|
// When querying parents of "grandchild", we get: grandchild -> child -> grandparent -> root
|
|
// This means "grandparent" is in the ancestry, so we can't move it under "grandchild"
|
|
parents: &folders.FolderInfoList{
|
|
Items: []folders.FolderInfo{
|
|
{Name: "grandchild", Parent: "child"},
|
|
{Name: "child", Parent: "grandparent"},
|
|
{Name: "grandparent", Parent: folder.GeneralFolderUID},
|
|
{Name: folder.GeneralFolderUID},
|
|
},
|
|
},
|
|
expectedErr: "cannot move folder under its own descendant",
|
|
},
|
|
{
|
|
name: "error when moving folder from root to level2 with children exceeds max depth",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "folderWithChildren",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "level2",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "folder with children",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "folderWithChildren",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "folder with children",
|
|
},
|
|
},
|
|
parents: &folders.FolderInfoList{
|
|
Items: []folders.FolderInfo{
|
|
{Name: "level2", Parent: "level1"},
|
|
{Name: "level1", Parent: folder.GeneralFolderUID},
|
|
{Name: folder.GeneralFolderUID},
|
|
},
|
|
},
|
|
allFolders: []folders.Folder{
|
|
{ObjectMeta: metav1.ObjectMeta{Name: "child1", Annotations: map[string]string{utils.AnnoKeyFolder: "folderWithChildren"}}},
|
|
{ObjectMeta: metav1.ObjectMeta{Name: "grandchild1", Annotations: map[string]string{utils.AnnoKeyFolder: "child1"}}},
|
|
},
|
|
maxDepth: 4,
|
|
expectedErr: "[folder.maximum-depth-reached]",
|
|
},
|
|
{
|
|
name: "can move folder from root level to level1 with children when within max depth",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "folderWithChildren",
|
|
Annotations: map[string]string{
|
|
utils.AnnoKeyFolder: "level1",
|
|
},
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "folder with children",
|
|
},
|
|
},
|
|
old: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "folderWithChildren",
|
|
},
|
|
Spec: folders.FolderSpec{
|
|
Title: "folder with children",
|
|
},
|
|
},
|
|
parents: &folders.FolderInfoList{
|
|
Items: []folders.FolderInfo{
|
|
{Name: "level1", Parent: folder.GeneralFolderUID},
|
|
{Name: folder.GeneralFolderUID},
|
|
},
|
|
},
|
|
allFolders: []folders.Folder{
|
|
{ObjectMeta: metav1.ObjectMeta{Name: "child1", Annotations: map[string]string{utils.AnnoKeyFolder: "folderWithChildren"}}},
|
|
{ObjectMeta: metav1.ObjectMeta{Name: "grandchild1", Annotations: map[string]string{utils.AnnoKeyFolder: "child1"}}},
|
|
},
|
|
maxDepth: 4,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
maxDepth := tt.maxDepth
|
|
if maxDepth == 0 {
|
|
maxDepth = 5
|
|
}
|
|
m := grafanarest.NewMockStorage(t)
|
|
if tt.parents != nil {
|
|
for _, v := range tt.parents.Items {
|
|
m.On("Get", context.Background(), v.Name, &metav1.GetOptions{}).Return(&folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: v.Name,
|
|
}, Spec: folders.FolderSpec{
|
|
Title: v.Title,
|
|
},
|
|
}, nil).Maybe()
|
|
}
|
|
}
|
|
for i := range tt.allFolders {
|
|
f := tt.allFolders[i]
|
|
m.On("Get", context.Background(), f.Name, &metav1.GetOptions{}).Return(&f, nil).Maybe()
|
|
}
|
|
|
|
err := validateOnUpdate(context.Background(), tt.folder, tt.old, m,
|
|
func(ctx context.Context, folder *folders.Folder) (*folders.FolderInfoList, error) {
|
|
return tt.parents, tt.parentsError
|
|
},
|
|
&mockSearchClient{folders: tt.allFolders},
|
|
maxDepth)
|
|
|
|
if tt.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateDelete(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
folder *folders.Folder
|
|
searcher *mockSearchClient
|
|
expectedErr string
|
|
}{{
|
|
name: "simple delete",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
// Empty stats
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{},
|
|
},
|
|
},
|
|
}, {
|
|
name: "stats error - nil stats",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{},
|
|
},
|
|
expectedErr: "could not verify if folder is empty",
|
|
}, {
|
|
name: "stats error - search error",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
statsErr: fmt.Errorf("error running stats"),
|
|
},
|
|
expectedErr: "error running stats",
|
|
}, {
|
|
name: "stats error - error result",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Error: &resourcepb.ErrorResult{
|
|
Reason: "error",
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "could not verify if folder is empty",
|
|
}, {
|
|
name: "folder not empty - contains dashboards",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{
|
|
{
|
|
Group: "dashboard.grafana.app",
|
|
Resource: "dashboards",
|
|
Count: 10, // not empty
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "[folder.not-empty]",
|
|
}, {
|
|
name: "folder not empty - contains alertrules",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{
|
|
{
|
|
Group: "alerting.grafana.app",
|
|
Resource: "alertrules",
|
|
Count: 5, // not empty
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "[folder.not-empty]",
|
|
}, {
|
|
name: "folder not empty - contains library_elements",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{
|
|
{
|
|
Group: "library.grafana.app",
|
|
Resource: "library_elements",
|
|
Count: 3, // not empty
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "[folder.not-empty]",
|
|
}, {
|
|
name: "folder not empty - contains folders",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{
|
|
{
|
|
Group: "folders.grafana.app",
|
|
Resource: "folders",
|
|
Count: 2, // not empty
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "[folder.not-empty]",
|
|
}, {
|
|
name: "folder can be deleted when it only contains non-validated resource types",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{
|
|
{
|
|
Group: "playlist.grafana.app",
|
|
Resource: "playlists",
|
|
Count: 10, // has content but not a validated resource type
|
|
},
|
|
{
|
|
Group: "other.grafana.app",
|
|
Resource: "other",
|
|
Count: 5, // has content but not a validated resource type
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, {
|
|
name: "folder not empty - mixed resources with validated types",
|
|
folder: &folders.Folder{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "nnn",
|
|
},
|
|
},
|
|
searcher: &mockSearchClient{
|
|
stats: &resourcepb.ResourceStatsResponse{
|
|
Stats: []*resourcepb.ResourceStatsResponse_Stats{
|
|
{
|
|
Group: "folders.grafana.app",
|
|
Resource: "folders",
|
|
Count: 10, // now validated
|
|
},
|
|
{
|
|
Group: "dashboard.grafana.app",
|
|
Resource: "dashboards",
|
|
Count: 2, // validated and has content
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedErr: "[folder.not-empty]",
|
|
}}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateOnDelete(context.Background(), tt.folder, tt.searcher)
|
|
|
|
if tt.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tt.expectedErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var (
|
|
_ = resourcepb.ResourceIndexClient(&mockSearchClient{})
|
|
)
|
|
|
|
type mockSearchClient struct {
|
|
stats *resourcepb.ResourceStatsResponse
|
|
statsErr error
|
|
|
|
folders []folders.Folder
|
|
}
|
|
|
|
// GetStats implements resourcepb.ResourceIndexClient.
|
|
func (m *mockSearchClient) GetStats(ctx context.Context, in *resourcepb.ResourceStatsRequest, opts ...grpc.CallOption) (*resourcepb.ResourceStatsResponse, error) {
|
|
return m.stats, m.statsErr
|
|
}
|
|
|
|
// Search implements resourcepb.ResourceIndexClient.
|
|
func (m *mockSearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, opts ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
|
|
// get the list of parents from the search request
|
|
parentSet := make(map[string]bool)
|
|
if req.Options != nil && req.Options.Fields != nil {
|
|
for _, field := range req.Options.Fields {
|
|
if field.Key == "folder" && field.Operator == "in" {
|
|
for _, v := range field.Values {
|
|
parentSet[v] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// find children that match the parent filter
|
|
var rows []*resourcepb.ResourceTableRow
|
|
for i := range m.folders {
|
|
meta, err := utils.MetaAccessor(&m.folders[i])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
parentUID := meta.GetFolder()
|
|
if parentSet[parentUID] {
|
|
rows = append(rows, &resourcepb.ResourceTableRow{
|
|
Key: &resourcepb.ResourceKey{Name: m.folders[i].Name},
|
|
})
|
|
}
|
|
}
|
|
|
|
return &resourcepb.ResourceSearchResponse{
|
|
Results: &resourcepb.ResourceTable{Rows: rows},
|
|
}, nil
|
|
}
|
|
|
|
// RebuildIndexes implements resourcepb.ResourceIndexClient.
|
|
func (m *mockSearchClient) RebuildIndexes(ctx context.Context, in *resourcepb.RebuildIndexesRequest, opts ...grpc.CallOption) (*resourcepb.RebuildIndexesResponse, error) {
|
|
return nil, fmt.Errorf("not implemented")
|
|
}
|