mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
987 lines
32 KiB
987 lines
32 KiB
package folderimpl
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/dynamic"
|
|
clientrest "k8s.io/client-go/rest"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
|
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/guardian"
|
|
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/services/publicdashboards"
|
|
"github.com/grafana/grafana/pkg/services/search/model"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
|
)
|
|
|
|
type rcp struct {
|
|
Host string
|
|
}
|
|
|
|
func (r rcp) GetRestConfig(ctx context.Context) *clientrest.Config {
|
|
return &clientrest.Config{
|
|
Host: r.Host,
|
|
}
|
|
}
|
|
|
|
func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
m := map[string]v0alpha1.Folder{}
|
|
|
|
unifiedStorageFolder := &v0alpha1.Folder{}
|
|
unifiedStorageFolder.Kind = "folder"
|
|
|
|
fooFolder := &folder.Folder{
|
|
ID: 123,
|
|
Title: "Foo Folder",
|
|
OrgID: orgID,
|
|
UID: "foo",
|
|
URL: "/dashboards/f/foo/foo-folder",
|
|
CreatedByUID: "user:1",
|
|
UpdatedByUID: "user:1",
|
|
}
|
|
|
|
updateFolder := &folder.Folder{
|
|
Title: "Folder",
|
|
OrgID: orgID,
|
|
UID: "updatefolder",
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("DELETE /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/deletefolder", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
})
|
|
|
|
mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
l := &v0alpha1.FolderList{}
|
|
l.Kind = "Folder"
|
|
err := json.NewEncoder(w).Encode(l)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/foo", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
namespacer := func(_ int64) string { return "1" }
|
|
result, err := internalfolders.LegacyFolderToUnstructured(fooFolder, namespacer)
|
|
require.NoError(t, err)
|
|
|
|
err = json.NewEncoder(w).Encode(result)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/updatefolder", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
namespacer := func(_ int64) string { return "1" }
|
|
result, err := internalfolders.LegacyFolderToUnstructured(updateFolder, namespacer)
|
|
require.NoError(t, err)
|
|
|
|
err = json.NewEncoder(w).Encode(result)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/updatefolder", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
buf, err := io.ReadAll(req.Body)
|
|
require.NoError(t, err)
|
|
|
|
var foldr v0alpha1.Folder
|
|
err = json.Unmarshal(buf, &foldr)
|
|
require.NoError(t, err)
|
|
|
|
updateFolder.Title = foldr.Spec.Title
|
|
|
|
namespacer := func(_ int64) string { return "1" }
|
|
result, err := internalfolders.LegacyFolderToUnstructured(updateFolder, namespacer)
|
|
require.NoError(t, err)
|
|
|
|
err = json.NewEncoder(w).Encode(result)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err := json.NewEncoder(w).Encode(unifiedStorageFolder)
|
|
require.NoError(t, err)
|
|
})
|
|
mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
err := json.NewEncoder(w).Encode(unifiedStorageFolder)
|
|
require.NoError(t, err)
|
|
})
|
|
mux.HandleFunc("POST /apis/folder.grafana.app/v0alpha1/namespaces/default/folders", func(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
buf, err := io.ReadAll(req.Body)
|
|
require.NoError(t, err)
|
|
|
|
var folder v0alpha1.Folder
|
|
err = json.Unmarshal(buf, &folder)
|
|
require.NoError(t, err)
|
|
|
|
m[folder.Name] = folder
|
|
|
|
fmt.Printf("buf: %+v\n", folder)
|
|
folder.Kind = "Folder"
|
|
err = json.NewEncoder(w).Encode(folder)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
folderApiServerMock := httptest.NewServer(mux)
|
|
defer folderApiServerMock.Close()
|
|
|
|
origNewGuardian := guardian.New
|
|
t.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
})
|
|
|
|
db, cfg := sqlstore.InitTestDB(t)
|
|
cfg.AppURL = folderApiServerMock.URL
|
|
|
|
restCfgProvider := rcp{
|
|
Host: folderApiServerMock.URL,
|
|
}
|
|
|
|
f := func(ctx context.Context) resource.ResourceClient {
|
|
return resourceClientMock{}
|
|
}
|
|
|
|
k8sHandler := &foldk8sHandler{
|
|
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
|
namespacer: request.GetNamespaceMapper(cfg),
|
|
cfg: cfg,
|
|
restConfigProvider: restCfgProvider.GetRestConfig,
|
|
recourceClientProvider: f,
|
|
}
|
|
|
|
userService := &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{},
|
|
}
|
|
|
|
unifiedStore := ProvideUnifiedStore(k8sHandler, userService)
|
|
|
|
ctx := context.Background()
|
|
usr := &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
|
1: accesscontrol.GroupScopesByActionContext(
|
|
ctx,
|
|
[]accesscontrol.Permission{
|
|
{Action: dashboards.ActionFoldersCreate, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: dashboards.ActionFoldersWrite, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: accesscontrol.ActionAlertingRuleDelete, Scope: dashboards.ScopeFoldersAll},
|
|
}),
|
|
}}
|
|
|
|
alertingStore := ngstore.DBstore{
|
|
SQLStore: db,
|
|
Cfg: cfg.UnifiedAlerting,
|
|
Logger: log.New("test-alerting-store"),
|
|
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
|
|
}
|
|
|
|
featuresArr := []any{
|
|
featuremgmt.FlagKubernetesFoldersServiceV2}
|
|
features := featuremgmt.WithFeatures(featuresArr...)
|
|
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
|
publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
|
|
|
folderService := &Service{
|
|
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
|
unifiedStore: unifiedStore,
|
|
features: features,
|
|
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
|
accessControl: acimpl.ProvideAccessControl(features),
|
|
registry: make(map[string]folder.RegistryService),
|
|
metrics: newFoldersMetrics(nil),
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
k8sclient: k8sHandler,
|
|
dashboardStore: dashboardStore,
|
|
publicDashboardService: publicDashboardService,
|
|
}
|
|
|
|
require.NoError(t, folderService.RegisterService(alertingStore))
|
|
|
|
t.Run("Folder service tests", func(t *testing.T) {
|
|
t.Run("Given user has no permissions", func(t *testing.T) {
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{})
|
|
|
|
ctx = identity.WithRequester(context.Background(), noPermUsr)
|
|
|
|
f := folder.NewFolder("Folder", "")
|
|
f.UID = "foo"
|
|
|
|
t.Run("When get folder by id should return access denied error", func(t *testing.T) {
|
|
_, err := folderService.Get(ctx, &folder.GetFolderQuery{
|
|
UID: &f.UID,
|
|
OrgID: orgID,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Equal(t, dashboards.ErrFolderAccessDenied, err)
|
|
})
|
|
|
|
t.Run("When get folder by uid should return access denied error", func(t *testing.T) {
|
|
_, err := folderService.Get(ctx, &folder.GetFolderQuery{
|
|
UID: &f.UID,
|
|
OrgID: orgID,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Equal(t, dashboards.ErrFolderAccessDenied, err)
|
|
})
|
|
|
|
t.Run("When creating folder should return access denied error", func(t *testing.T) {
|
|
_, err := folderService.Create(ctx, &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: f.Title,
|
|
UID: f.UID,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
title := "Folder-TEST"
|
|
t.Run("When updating folder should return access denied error", func(t *testing.T) {
|
|
_, err := folderService.Update(ctx, &folder.UpdateFolderCommand{
|
|
UID: f.UID,
|
|
OrgID: orgID,
|
|
NewTitle: &title,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Error(t, err)
|
|
require.Equal(t, dashboards.ErrFolderAccessDenied, err)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid should return access denied error", func(t *testing.T) {
|
|
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
|
UID: f.UID,
|
|
OrgID: orgID,
|
|
ForceDeleteRules: false,
|
|
SignedInUser: noPermUsr,
|
|
})
|
|
require.Error(t, err)
|
|
require.Equal(t, dashboards.ErrFolderAccessDenied, err)
|
|
})
|
|
|
|
t.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
})
|
|
})
|
|
|
|
t.Run("Given user has permission to save", func(t *testing.T) {
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true, CanViewValue: true})
|
|
|
|
ctx = identity.WithRequester(context.Background(), usr)
|
|
|
|
f := &folder.Folder{
|
|
OrgID: orgID,
|
|
Title: "Test-Folder",
|
|
UID: "testfolder",
|
|
URL: "/dashboards/f/testfolder/test-folder",
|
|
CreatedByUID: "user:1",
|
|
UpdatedByUID: "user:1",
|
|
}
|
|
|
|
t.Run("When creating folder should not return access denied error", func(t *testing.T) {
|
|
actualFolder, err := folderService.Create(ctx, &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: f.Title,
|
|
UID: f.UID,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, f, actualFolder)
|
|
})
|
|
|
|
t.Run("When creating folder should return error if uid is general", func(t *testing.T) {
|
|
_, err := folderService.Create(ctx, &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
Title: f.Title,
|
|
UID: "general",
|
|
SignedInUser: usr,
|
|
})
|
|
require.ErrorIs(t, err, dashboards.ErrFolderInvalidUID)
|
|
})
|
|
|
|
t.Run("When updating folder should not return access denied error", func(t *testing.T) {
|
|
title := "TEST-Folder"
|
|
req := &folder.UpdateFolderCommand{
|
|
UID: updateFolder.UID,
|
|
OrgID: orgID,
|
|
NewTitle: &title,
|
|
SignedInUser: usr,
|
|
}
|
|
|
|
reqResult, err := folderService.Update(ctx, req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, title, reqResult.Title)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid should not return access denied error - ForceDeleteRules true", func(t *testing.T) {
|
|
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
|
UID: "deletefolder",
|
|
OrgID: orgID,
|
|
ForceDeleteRules: true,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid should not return access denied error - ForceDeleteRules false", func(t *testing.T) {
|
|
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil)
|
|
publicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
|
UID: "deletefolder",
|
|
OrgID: orgID,
|
|
ForceDeleteRules: false,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid, expectedForceDeleteRules as false, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
|
|
folderService.features = featuremgmt.WithFeatures(append(featuresArr, featuremgmt.FlagDashboardRestore)...)
|
|
|
|
expectedForceDeleteRules := false
|
|
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
|
UID: "deletefolder",
|
|
OrgID: orgID,
|
|
ForceDeleteRules: expectedForceDeleteRules,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When deleting folder by uid, expectedForceDeleteRules as true, and dashboard Restore turned on should not return access denied error", func(t *testing.T) {
|
|
folderService.features = featuremgmt.WithFeatures(append(featuresArr, featuremgmt.FlagDashboardRestore)...)
|
|
|
|
expectedForceDeleteRules := true
|
|
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
|
UID: "deletefolder",
|
|
OrgID: orgID,
|
|
ForceDeleteRules: expectedForceDeleteRules,
|
|
SignedInUser: usr,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
})
|
|
})
|
|
|
|
t.Run("Given user has permission to view", func(t *testing.T) {
|
|
origNewGuardian := guardian.New
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true})
|
|
|
|
t.Run("When get folder by uid should return folder", func(t *testing.T) {
|
|
actual, err := folderService.Get(ctx, &folder.GetFolderQuery{
|
|
UID: &fooFolder.UID,
|
|
OrgID: fooFolder.OrgID,
|
|
SignedInUser: usr,
|
|
})
|
|
require.Equal(t, fooFolder, actual)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When get folder by uid and uid is general should return the root folder object", func(t *testing.T) {
|
|
uid := accesscontrol.GeneralFolderUID
|
|
query := &folder.GetFolderQuery{
|
|
UID: &uid,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
}
|
|
actual, err := folderService.Get(ctx, query)
|
|
require.Equal(t, folder.RootFolder, actual)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When get folder by ID and uid is an empty string should return folder by id", func(t *testing.T) {
|
|
id := int64(123)
|
|
emptyString := ""
|
|
query := &folder.GetFolderQuery{
|
|
UID: &emptyString,
|
|
ID: &id,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
}
|
|
|
|
actual, err := folderService.Get(context.Background(), query)
|
|
require.Equal(t, fooFolder, actual)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When get folder by non existing ID should return not found error", func(t *testing.T) {
|
|
id := int64(111111)
|
|
query := &folder.GetFolderQuery{
|
|
ID: &id,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
}
|
|
|
|
actual, err := folderService.Get(context.Background(), query)
|
|
require.Nil(t, actual)
|
|
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
|
|
})
|
|
|
|
t.Run("When get folder by Title should return folder", func(t *testing.T) {
|
|
title := "foo"
|
|
query := &folder.GetFolderQuery{
|
|
Title: &title,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
}
|
|
|
|
actual, err := folderService.Get(context.Background(), query)
|
|
require.Equal(t, fooFolder, actual)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("When get folder by non existing Title should return not found error", func(t *testing.T) {
|
|
title := "does not exists"
|
|
query := &folder.GetFolderQuery{
|
|
Title: &title,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
}
|
|
|
|
actual, err := folderService.Get(context.Background(), query)
|
|
require.Nil(t, actual)
|
|
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
|
|
})
|
|
|
|
t.Cleanup(func() {
|
|
guardian.New = origNewGuardian
|
|
})
|
|
})
|
|
|
|
t.Run("Returns root folder", func(t *testing.T) {
|
|
t.Run("When the folder UID and title are blank, and id is 0, should return the root folder", func(t *testing.T) {
|
|
emptyString := ""
|
|
idZero := int64(0)
|
|
actual, err := folderService.Get(ctx, &folder.GetFolderQuery{
|
|
UID: &emptyString,
|
|
ID: &idZero,
|
|
Title: &emptyString,
|
|
OrgID: 1,
|
|
SignedInUser: usr,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, folder.GeneralFolder.UID, actual.UID)
|
|
require.Equal(t, folder.GeneralFolder.Title, actual.Title)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
type resourceClientMock struct{}
|
|
|
|
func (r resourceClientMock) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
|
|
if len(in.Options.Labels) > 0 &&
|
|
in.Options.Labels[0].Key == utils.LabelKeyDeprecatedInternalID &&
|
|
in.Options.Labels[0].Operator == "in" &&
|
|
len(in.Options.Labels[0].Values) > 0 &&
|
|
in.Options.Labels[0].Values[0] == "123" {
|
|
return &resource.ResourceSearchResponse{
|
|
Results: &resource.ResourceTable{
|
|
Columns: []*resource.ResourceTableColumnDefinition{
|
|
{
|
|
Name: "_id",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "title",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "folder",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
},
|
|
Rows: []*resource.ResourceTableRow{
|
|
{
|
|
Key: &resource.ResourceKey{
|
|
Name: "foo",
|
|
Resource: "folders",
|
|
},
|
|
Cells: [][]byte{
|
|
[]byte("123"),
|
|
[]byte("folder1"),
|
|
[]byte(""),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TotalHits: 1,
|
|
}, nil
|
|
}
|
|
|
|
if len(in.Options.Fields) > 0 &&
|
|
in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE_PHRASE &&
|
|
in.Options.Fields[0].Operator == "in" &&
|
|
len(in.Options.Fields[0].Values) > 0 &&
|
|
in.Options.Fields[0].Values[0] == "foo" {
|
|
return &resource.ResourceSearchResponse{
|
|
Results: &resource.ResourceTable{
|
|
Columns: []*resource.ResourceTableColumnDefinition{
|
|
{
|
|
Name: "_id",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "title",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "folder",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
},
|
|
Rows: []*resource.ResourceTableRow{
|
|
{
|
|
Key: &resource.ResourceKey{
|
|
Name: "foo",
|
|
Resource: "folders",
|
|
},
|
|
Cells: [][]byte{
|
|
[]byte("123"),
|
|
[]byte("folder1"),
|
|
[]byte(""),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TotalHits: 1,
|
|
}, nil
|
|
}
|
|
|
|
if in.Query == "*test*" {
|
|
return &resource.ResourceSearchResponse{
|
|
Results: &resource.ResourceTable{
|
|
Columns: []*resource.ResourceTableColumnDefinition{
|
|
{
|
|
Name: "_id",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "title",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "folder",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
},
|
|
Rows: []*resource.ResourceTableRow{
|
|
{
|
|
Key: &resource.ResourceKey{
|
|
Name: "uid",
|
|
Resource: "folders",
|
|
},
|
|
Cells: [][]byte{
|
|
[]byte("123"),
|
|
[]byte("testing-123"),
|
|
[]byte("parent-uid"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
TotalHits: 1,
|
|
}, nil
|
|
}
|
|
|
|
if len(in.Options.Fields) > 0 &&
|
|
in.Options.Fields[0].Key == resource.SEARCH_FIELD_NAME &&
|
|
in.Options.Fields[0].Operator == "in" &&
|
|
len(in.Options.Fields[0].Values) > 0 {
|
|
rows := []*resource.ResourceTableRow{}
|
|
for i, row := range in.Options.Fields[0].Values {
|
|
rows = append(rows, &resource.ResourceTableRow{
|
|
Key: &resource.ResourceKey{
|
|
Name: row,
|
|
Resource: "folders",
|
|
},
|
|
Cells: [][]byte{
|
|
[]byte(fmt.Sprintf("%d", i)), // set legacy id as the row id
|
|
[]byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id
|
|
[]byte(""),
|
|
},
|
|
})
|
|
}
|
|
return &resource.ResourceSearchResponse{
|
|
Results: &resource.ResourceTable{
|
|
Columns: []*resource.ResourceTableColumnDefinition{
|
|
{
|
|
Name: "_id",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "title",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "folder",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
},
|
|
Rows: rows,
|
|
},
|
|
TotalHits: int64(len(rows)),
|
|
}, nil
|
|
}
|
|
|
|
if len(in.Options.Fields) > 0 &&
|
|
in.Options.Fields[0].Key == resource.SEARCH_FIELD_FOLDER &&
|
|
in.Options.Fields[0].Operator == "in" &&
|
|
len(in.Options.Fields[0].Values) > 0 {
|
|
rows := []*resource.ResourceTableRow{}
|
|
for i, row := range in.Options.Fields[0].Values {
|
|
rows = append(rows, &resource.ResourceTableRow{
|
|
Key: &resource.ResourceKey{
|
|
Name: row,
|
|
Resource: "folders",
|
|
},
|
|
Cells: [][]byte{
|
|
[]byte(fmt.Sprintf("%d", i)), // set legacy id as the row id
|
|
[]byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id
|
|
[]byte(""),
|
|
},
|
|
})
|
|
}
|
|
return &resource.ResourceSearchResponse{
|
|
Results: &resource.ResourceTable{
|
|
Columns: []*resource.ResourceTableColumnDefinition{
|
|
{
|
|
Name: "_id",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "title",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
{
|
|
Name: "folder",
|
|
Type: resource.ResourceTableColumnDefinition_STRING,
|
|
},
|
|
},
|
|
Rows: rows,
|
|
},
|
|
TotalHits: int64(len(rows)),
|
|
}, nil
|
|
}
|
|
|
|
// not found
|
|
return &resource.ResourceSearchResponse{
|
|
Results: &resource.ResourceTable{},
|
|
}, nil
|
|
}
|
|
func (r resourceClientMock) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) {
|
|
return nil, nil
|
|
}
|
|
func (r resourceClientMock) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type mockFoldersK8sCli struct {
|
|
mock.Mock
|
|
searcher resourceClientMock
|
|
}
|
|
|
|
func (m *mockFoldersK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
|
args := m.Called(ctx, orgID)
|
|
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
|
|
}
|
|
|
|
func (m *mockFoldersK8sCli) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
|
args := m.Called(ctx, orgID)
|
|
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
|
|
}
|
|
|
|
func (m *mockFoldersK8sCli) getNamespace(orgID int64) string {
|
|
if orgID == 1 {
|
|
return "default"
|
|
}
|
|
return fmt.Sprintf("orgs-%d", orgID)
|
|
}
|
|
|
|
func (m *mockFoldersK8sCli) getSearcher(ctx context.Context) resource.ResourceClient {
|
|
return m.searcher
|
|
}
|
|
|
|
func TestSearchFoldersFromApiServer(t *testing.T) {
|
|
fakeK8sClient := new(mockFoldersK8sCli)
|
|
service := Service{
|
|
k8sclient: fakeK8sClient,
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
|
}
|
|
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
|
|
user := &user.SignedInUser{OrgID: 1}
|
|
ctx := identity.WithRequester(context.Background(), user)
|
|
|
|
t.Run("Should search by uids if provided", func(t *testing.T) {
|
|
query := folder.SearchFoldersQuery{
|
|
UIDs: []string{"uid1", "uid2"},
|
|
IDs: []int64{1, 2}, // will ignore these because uid is passed in
|
|
SignedInUser: user,
|
|
}
|
|
result, err := service.searchFoldersFromApiServer(ctx, query)
|
|
require.NoError(t, err)
|
|
|
|
expectedResult := model.HitList{
|
|
{
|
|
UID: "uid1",
|
|
// no parent folder is returned, so the general folder should be set
|
|
FolderID: 0,
|
|
FolderTitle: "General",
|
|
// orgID should be taken from signed in user
|
|
OrgID: 1,
|
|
// the rest should be automatically set when parsing the hit results from search
|
|
Type: model.DashHitFolder,
|
|
URI: "db/folder0",
|
|
Title: "folder0",
|
|
URL: "/dashboards/f/uid1/folder0",
|
|
},
|
|
{
|
|
UID: "uid2",
|
|
FolderID: 0,
|
|
FolderTitle: "General",
|
|
OrgID: 1,
|
|
Type: model.DashHitFolder,
|
|
URI: "db/folder1",
|
|
Title: "folder1",
|
|
URL: "/dashboards/f/uid2/folder1",
|
|
},
|
|
}
|
|
require.Equal(t, expectedResult, result)
|
|
})
|
|
|
|
t.Run("Search by ID if uids are not provided", func(t *testing.T) {
|
|
query := folder.SearchFoldersQuery{
|
|
IDs: []int64{123},
|
|
SignedInUser: user,
|
|
}
|
|
result, err := service.searchFoldersFromApiServer(ctx, query)
|
|
require.NoError(t, err)
|
|
|
|
expectedResult := model.HitList{
|
|
{
|
|
UID: "foo",
|
|
FolderID: 0,
|
|
FolderTitle: "General",
|
|
OrgID: 1,
|
|
Type: model.DashHitFolder,
|
|
URI: "db/folder1",
|
|
Title: "folder1",
|
|
URL: "/dashboards/f/foo/folder1",
|
|
},
|
|
}
|
|
require.Equal(t, expectedResult, result)
|
|
})
|
|
|
|
t.Run("Search by title, wildcard should be added to search request (won't match in search mock if not)", func(t *testing.T) {
|
|
// the search here will return a parent, this will be the parent folder returned when we query for it to add to the hit info
|
|
fakeFolderStore := folder.NewFakeStore()
|
|
fakeFolderStore.ExpectedFolder = &folder.Folder{
|
|
UID: "parent-uid",
|
|
ID: 2,
|
|
Title: "parent title",
|
|
}
|
|
service.unifiedStore = fakeFolderStore
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
|
CanSaveValue: true,
|
|
CanViewValue: true,
|
|
})
|
|
|
|
query := folder.SearchFoldersQuery{
|
|
Title: "test",
|
|
SignedInUser: user,
|
|
}
|
|
result, err := service.searchFoldersFromApiServer(ctx, query)
|
|
require.NoError(t, err)
|
|
|
|
expectedResult := model.HitList{
|
|
{
|
|
UID: "uid",
|
|
FolderID: 2,
|
|
FolderTitle: "parent title",
|
|
FolderUID: "parent-uid",
|
|
OrgID: 1,
|
|
Type: model.DashHitFolder,
|
|
URI: "db/testing-123",
|
|
Title: "testing-123",
|
|
URL: "/dashboards/f/uid/testing-123",
|
|
},
|
|
}
|
|
require.Equal(t, expectedResult, result)
|
|
})
|
|
}
|
|
|
|
type mockDashboardCli struct {
|
|
mock.Mock
|
|
dynamic.ResourceInterface
|
|
}
|
|
|
|
func (c *mockDashboardCli) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
|
args := c.Called(ctx, name, options)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func TestDeleteFoldersFromApiServer(t *testing.T) {
|
|
fakeK8sClient := new(mockFoldersK8sCli)
|
|
fakeFolderStore := folder.NewFakeStore()
|
|
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
|
publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
|
service := Service{
|
|
k8sclient: fakeK8sClient,
|
|
unifiedStore: fakeFolderStore,
|
|
dashboardStore: dashboardStore,
|
|
publicDashboardService: publicDashboardFakeService,
|
|
registry: make(map[string]folder.RegistryService),
|
|
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
|
}
|
|
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
|
|
user := &user.SignedInUser{OrgID: 1}
|
|
ctx := identity.WithRequester(context.Background(), user)
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
|
CanSaveValue: true,
|
|
CanViewValue: true,
|
|
})
|
|
db, cfg := sqlstore.InitTestDB(t)
|
|
|
|
alertingStore := ngstore.DBstore{
|
|
SQLStore: db,
|
|
Cfg: cfg.UnifiedAlerting,
|
|
Logger: log.New("test-alerting-store"),
|
|
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
|
|
}
|
|
require.NoError(t, service.RegisterService(alertingStore))
|
|
|
|
t.Run("Should delete folder", func(t *testing.T) {
|
|
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
|
|
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
|
|
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
|
UID: "uid",
|
|
OrgID: 1,
|
|
SignedInUser: user,
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("Should delete dashboards and public dashboards within the folder", func(t *testing.T) {
|
|
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{
|
|
{
|
|
UID: "test",
|
|
OrgID: 1,
|
|
},
|
|
{
|
|
UID: "test2",
|
|
OrgID: 1,
|
|
},
|
|
}, nil).Once()
|
|
dashboardStore.On("DeleteDashboard", mock.Anything, &dashboards.DeleteDashboardCommand{
|
|
UID: "test",
|
|
OrgID: 1,
|
|
}).Return(nil).Once()
|
|
dashboardStore.On("DeleteDashboard", mock.Anything, &dashboards.DeleteDashboardCommand{
|
|
UID: "test2",
|
|
OrgID: 1,
|
|
}).Return(nil).Once()
|
|
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"test", "test2"}).Return(nil).Once()
|
|
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
|
UID: "uid",
|
|
OrgID: 1,
|
|
SignedInUser: user,
|
|
})
|
|
require.NoError(t, err)
|
|
dashboardStore.AssertExpectations(t)
|
|
publicDashboardFakeService.AssertExpectations(t)
|
|
})
|
|
|
|
// enable k8s ff for dashboards, retest
|
|
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagKubernetesCliDashboards)
|
|
|
|
t.Run("Should delete dashboards and public dashboards within the folder through k8s if the ff is enabled", func(t *testing.T) {
|
|
dashboardK8sCli := mockDashboardCli{}
|
|
dashboardK8sCli.On("Delete", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(nil).Once()
|
|
fakeK8sClient.On("getDashboardClient", mock.Anything, mock.Anything).Return(&dashboardK8sCli, true)
|
|
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
|
|
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"uid1"}).Return(nil).Once()
|
|
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
|
UID: "uid1",
|
|
OrgID: 1,
|
|
SignedInUser: user,
|
|
})
|
|
require.NoError(t, err)
|
|
dashboardStore.AssertExpectations(t)
|
|
publicDashboardFakeService.AssertExpectations(t)
|
|
dashboardK8sCli.AssertExpectations(t)
|
|
})
|
|
}
|
|
|