Filewalkwithme/unistore refactor folder service to hit folder apiserver (#98409)
Refactor folder service to use Unified Storage Signed-off-by: Maicon Costa <maiconscosta@gmail.com> --------- Signed-off-by: Maicon Costa <maiconscosta@gmail.com> Co-authored-by: Stephanie Hingtgen <stephanie.hingtgen@grafana.com>
This commit is contained in:
co-authored by
Stephanie Hingtgen
parent
009d7f42b3
commit
766d645d82
@@ -0,0 +1,414 @@
|
||||
package folderimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"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/authz/zanzana"
|
||||
"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/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
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{
|
||||
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
|
||||
|
||||
unifiedStore := ProvideUnifiedStore(cfg)
|
||||
|
||||
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.FlagKubernetesFolders,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2}
|
||||
features := featuremgmt.WithFeatures(featuresArr...)
|
||||
|
||||
folderService := &Service{
|
||||
log: slog.New(logtest.NewTestHandler(t)).With("logger", "test-folder-service"),
|
||||
unifiedStore: unifiedStore,
|
||||
features: features,
|
||||
bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
// db: db,
|
||||
accessControl: acimpl.ProvideAccessControl(features, zanzana.NewNoopClient()),
|
||||
registry: make(map[string]folder.RegistryService),
|
||||
metrics: newFoldersMetrics(nil),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
}
|
||||
|
||||
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) {
|
||||
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)
|
||||
})
|
||||
|
||||
// TODO!!
|
||||
/*
|
||||
t.Run("When get folder by title should return folder", func(t *testing.T) {
|
||||
expected := folder.NewFolder("TEST-"+util.GenerateShortUID(), "")
|
||||
|
||||
actual, err := service.getFolderByTitle(context.Background(), orgID, expected.Title, nil)
|
||||
require.Equal(t, expected, actual)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
*/
|
||||
|
||||
t.Cleanup(func() {
|
||||
guardian.New = origNewGuardian
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Returns root folder", func(t *testing.T) {
|
||||
t.Run("When the folder UID is blank should return the root folder", func(t *testing.T) {
|
||||
emptyString := ""
|
||||
actual, err := folderService.Get(ctx, &folder.GetFolderQuery{
|
||||
UID: &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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user