mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Unistore: Add GetFolders endpoint backed by UnifiedStorage (#96399)
* Unistore: Add GetFolders endpoint backed by UnifiedStorage Signed-off-by: Maicon Costa <maiconscosta@gmail.com> --------- Signed-off-by: Maicon Costa <maiconscosta@gmail.com> Co-authored-by: Arati R. <33031346+suntala@users.noreply.github.com>
This commit is contained in:
parent
706300e9b7
commit
bbae396db4
@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -45,7 +46,6 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
apiRoute.Group("/folders", func(folderRoute routing.RouteRegister) {
|
||||
idScope := dashboards.ScopeFoldersProvider.GetResourceScope(accesscontrol.Parameter(":id"))
|
||||
uidScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
folderRoute.Get("/id/:id", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead, idScope)), routing.Wrap(hs.GetFolderByID))
|
||||
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
@ -64,13 +64,14 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
// Use k8s client to implement legacy API
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Post("/", handler.createFolder)
|
||||
folderRoute.Get("/", handler.getFolders)
|
||||
} else {
|
||||
folderRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersCreate)), routing.Wrap(hs.CreateFolder))
|
||||
folderRoute.Get("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersRead)), routing.Wrap(hs.GetFolders))
|
||||
}
|
||||
// Only adding support for some routes with the k8s handler for now. Include the rest here.
|
||||
if false {
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Get("/", handler.searchFolders)
|
||||
folderRoute.Group("/:uid", func(folderUidRoute routing.RouteRegister) {
|
||||
folderUidRoute.Get("/", handler.getFolder)
|
||||
folderUidRoute.Delete("/", handler.deleteFolder)
|
||||
@ -661,32 +662,6 @@ func newFolderK8sHandler(hs *HTTPServer) *folderK8sHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) searchFolders(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
out, err := client.List(c.Req.Context(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
query := strings.ToUpper(c.Query("query"))
|
||||
folders := []folder.Folder{}
|
||||
for _, item := range out.Items {
|
||||
p, _ := internalfolders.UnstructuredToLegacyFolder(item, c.SignedInUser.GetOrgID())
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if query != "" && !strings.Contains(strings.ToUpper(p.Title), query) {
|
||||
continue // query filter
|
||||
}
|
||||
folders = append(folders, *p)
|
||||
}
|
||||
c.JSON(http.StatusOK, folders)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
@ -718,6 +693,62 @@ func (fk8s *folderK8sHandler) createFolder(c *contextmodel.ReqContext) {
|
||||
c.JSON(http.StatusOK, folderDTO)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getFolders(c *contextmodel.ReqContext) {
|
||||
// NOTE: the current implementation is temporary and it will be
|
||||
// replaced by a proper indexing service/search API
|
||||
// Also, the current implementation does not support pagination
|
||||
|
||||
parentUid := strings.ToUpper(c.Query("parentUid"))
|
||||
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
return // error is already sent
|
||||
}
|
||||
|
||||
// check that parent exists
|
||||
if parentUid != "" {
|
||||
_, err := client.Get(c.Req.Context(), c.Query("parentUid"), v1.GetOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
out, err := client.List(c.Req.Context(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
fk8s.writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
hits := make([]dtos.FolderSearchHit, 0)
|
||||
for _, item := range out.Items {
|
||||
// convert item to legacy folder format
|
||||
f, _ := internalfolders.UnstructuredToLegacyFolder(item, c.SignedInUser.GetOrgID())
|
||||
if f == nil {
|
||||
fk8s.writeError(c, fmt.Errorf("unable covert unstructured item to legacy folder"))
|
||||
return
|
||||
}
|
||||
|
||||
// it we are at root level, skip subfolder
|
||||
if parentUid == "" && f.ParentUID != "" {
|
||||
continue // query filter
|
||||
}
|
||||
// if we are at a nested folder, then skip folders that don't belong to parentUid
|
||||
if parentUid != "" && strings.ToUpper(f.ParentUID) != parentUid {
|
||||
continue
|
||||
}
|
||||
|
||||
hits = append(hits, dtos.FolderSearchHit{
|
||||
ID: f.ID, // nolint:staticcheck
|
||||
UID: f.UID,
|
||||
Title: f.Title,
|
||||
ParentUID: f.ParentUID,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, hits)
|
||||
}
|
||||
|
||||
func (fk8s *folderK8sHandler) getFolder(c *contextmodel.ReqContext) {
|
||||
client, ok := fk8s.getClient(c)
|
||||
if !ok {
|
||||
|
@ -189,7 +189,7 @@ func (b *FolderAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorizerParams, error) {
|
||||
verb := attr.GetVerb()
|
||||
name := attr.GetName()
|
||||
if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate) {
|
||||
if (!attr.IsResourceRequest()) || (name == "" && verb != utils.VerbCreate && verb != utils.VerbList) {
|
||||
return nil, errNoResource
|
||||
}
|
||||
|
||||
@ -214,6 +214,8 @@ func authorizerFunc(ctx context.Context, attr authorizer.Attributes) (*authorize
|
||||
fallthrough
|
||||
case utils.VerbDelete:
|
||||
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersDelete, scope)
|
||||
case utils.VerbList:
|
||||
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead)
|
||||
default:
|
||||
eval = accesscontrol.EvalPermission(dashboards.ActionFoldersRead, scope)
|
||||
}
|
||||
|
@ -95,6 +95,42 @@ func TestFolderAPIBuilder_getAuthorizerFunc(t *testing.T) {
|
||||
eval: "folders:create",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user with read permissions should be able to list folders",
|
||||
input: input{
|
||||
user: &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: orgID,
|
||||
Name: "123",
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: {dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}},
|
||||
},
|
||||
},
|
||||
verb: string(utils.VerbList),
|
||||
},
|
||||
expect: expect{
|
||||
eval: "folders:read",
|
||||
allow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user without read permissions should not be able to list folders",
|
||||
input: input{
|
||||
user: &user.SignedInUser{
|
||||
UserID: 1,
|
||||
OrgID: orgID,
|
||||
Name: "123",
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: {},
|
||||
},
|
||||
},
|
||||
verb: string(utils.VerbList),
|
||||
},
|
||||
expect: expect{
|
||||
eval: "folders:read",
|
||||
allow: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b := &FolderAPIBuilder{
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@ -412,7 +413,7 @@ func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper
|
||||
require.JSONEq(t, expectedResult, client.SanitizeJSON(found))
|
||||
})
|
||||
|
||||
t.Run("Do CRUD (just CR for now) via k8s (and check that legacy api still works)", func(t *testing.T) {
|
||||
t.Run("Do CRUD (just CR+List for now) via k8s (and check that legacy api still works)", func(t *testing.T) {
|
||||
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
// #TODO: figure out permissions topic
|
||||
User: helper.Org1.Admin,
|
||||
@ -462,6 +463,15 @@ func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper
|
||||
require.Equal(t, "New description", description)
|
||||
// #TODO figure out why this breaks just for MySQL integration tests
|
||||
// require.Less(t, first.GetResourceVersion(), updated.GetResourceVersion())
|
||||
|
||||
// ensure that we get 4 items when listing via k8s
|
||||
l, err := client.Resource.List(context.Background(), metav1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, l)
|
||||
folders, err := meta.ExtractList(l)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, folders)
|
||||
require.Equal(t, len(folders), 4)
|
||||
})
|
||||
return helper
|
||||
}
|
||||
@ -926,3 +936,165 @@ func testDescription(description string, expectedErr error) string {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
// There are no counterpart of TestFoldersGetAPIEndpointK8S in pkg/api/folder_test.go
|
||||
func TestFoldersGetAPIEndpointK8S(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
expectedCode int
|
||||
params string
|
||||
createFolders []string
|
||||
expectedOutput []dtos.FolderSearchHit
|
||||
permissions []resourcepermissions.SetResourcePermissionCommand
|
||||
requestToAnotherOrg bool
|
||||
}
|
||||
|
||||
folderReadAndCreatePermission := []resourcepermissions.SetResourcePermissionCommand{
|
||||
{
|
||||
Actions: []string{"folders:create", "folders:read"},
|
||||
Resource: "folders",
|
||||
ResourceAttribute: "uid",
|
||||
ResourceID: "*",
|
||||
},
|
||||
}
|
||||
|
||||
folder1 := "{ \"uid\": \"foo\", \"title\": \"Folder 1\"}"
|
||||
folder2 := "{ \"uid\": \"bar\", \"title\": \"Folder 2\", \"parentUid\": \"foo\"}"
|
||||
folder3 := "{ \"uid\": \"qux\", \"title\": \"Folder 3\"}"
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
description: "listing folders at root level succeeds",
|
||||
createFolders: []string{
|
||||
folder1,
|
||||
folder2,
|
||||
folder3,
|
||||
},
|
||||
expectedCode: http.StatusOK,
|
||||
expectedOutput: []dtos.FolderSearchHit{
|
||||
dtos.FolderSearchHit{UID: "foo", Title: "Folder 1"},
|
||||
dtos.FolderSearchHit{UID: "qux", Title: "Folder 3"},
|
||||
},
|
||||
permissions: folderReadAndCreatePermission,
|
||||
},
|
||||
{
|
||||
description: "listing subfolders succeeds",
|
||||
createFolders: []string{
|
||||
folder1,
|
||||
folder2,
|
||||
folder3,
|
||||
},
|
||||
params: "?parentUid=foo",
|
||||
expectedCode: http.StatusOK,
|
||||
expectedOutput: []dtos.FolderSearchHit{
|
||||
dtos.FolderSearchHit{UID: "bar", Title: "Folder 2", ParentUID: "foo"},
|
||||
},
|
||||
permissions: folderReadAndCreatePermission,
|
||||
},
|
||||
{
|
||||
description: "listing subfolders for a parent that does not exists",
|
||||
createFolders: []string{
|
||||
folder1,
|
||||
folder2,
|
||||
folder3,
|
||||
},
|
||||
params: "?parentUid=notexists",
|
||||
expectedCode: http.StatusNotFound,
|
||||
expectedOutput: []dtos.FolderSearchHit{},
|
||||
permissions: folderReadAndCreatePermission,
|
||||
},
|
||||
{
|
||||
description: "listing folders at root level fails without the right permissions",
|
||||
createFolders: []string{
|
||||
folder1,
|
||||
folder2,
|
||||
folder3,
|
||||
},
|
||||
params: "?parentUid=notfound",
|
||||
expectedCode: http.StatusForbidden,
|
||||
expectedOutput: []dtos.FolderSearchHit{},
|
||||
permissions: folderReadAndCreatePermission,
|
||||
requestToAnotherOrg: true,
|
||||
},
|
||||
}
|
||||
|
||||
// test on all dualwriter modes
|
||||
for mode := 1; mode <= 4; mode++ {
|
||||
for _, tc := range tcs {
|
||||
t.Run(fmt.Sprintf("Mode: %d, %s", mode, tc.description), func(t *testing.T) {
|
||||
modeDw := grafanarest.DualWriterMode(mode)
|
||||
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: true,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: "unified",
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
folderv0alpha1.RESOURCEGROUP: {
|
||||
DualWriterMode: modeDw,
|
||||
},
|
||||
},
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs,
|
||||
featuremgmt.FlagNestedFolders,
|
||||
featuremgmt.FlagKubernetesFolders,
|
||||
},
|
||||
})
|
||||
|
||||
userTest := helper.CreateUser("user", apis.Org1, org.RoleNone, tc.permissions)
|
||||
|
||||
for _, f := range tc.createFolders {
|
||||
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||
User: userTest,
|
||||
GVR: gvr,
|
||||
})
|
||||
create2 := apis.DoRequest(helper, apis.RequestParams{
|
||||
User: client.Args.User,
|
||||
Method: http.MethodPost,
|
||||
Path: "/api/folders",
|
||||
Body: []byte(f),
|
||||
}, &folder.Folder{})
|
||||
require.NotEmpty(t, create2.Response)
|
||||
require.Equal(t, http.StatusOK, create2.Response.StatusCode)
|
||||
}
|
||||
|
||||
addr := helper.GetEnv().Server.HTTPServer.Listener.Addr()
|
||||
login := userTest.Identity.GetLogin()
|
||||
baseUrl := fmt.Sprintf("http://%s:%s@%s", login, user.Password("user"), addr)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(
|
||||
"%s%s",
|
||||
baseUrl,
|
||||
fmt.Sprintf("/api/folders%s", tc.params),
|
||||
), nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if tc.requestToAnotherOrg {
|
||||
req.Header.Set("x-grafana-org-id", "2")
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, tc.expectedCode, resp.StatusCode)
|
||||
|
||||
if tc.expectedCode == http.StatusOK {
|
||||
list := []dtos.FolderSearchHit{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&list)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
// ignore IDs
|
||||
for i := 0; i < len(list); i++ {
|
||||
list[i].ID = 0
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, tc.expectedOutput, list)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user