diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index 67076f2e96a..cc764ec3a4d 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -58,6 +58,9 @@ const AnnoKeySourcePath = "grafana.app/sourcePath" const AnnoKeySourceChecksum = "grafana.app/sourceChecksum" const AnnoKeySourceTimestamp = "grafana.app/sourceTimestamp" +const AnnoKeyFullpath = "grafana.app/fullpath" +const AnnoKeyFullpathUIDs = "grafana.app/fullpathUIDs" + // LabelKeyDeprecatedInternalID gives the deprecated internal ID of a resource // Deprecated: will be removed in grafana 13 const LabelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID" @@ -97,6 +100,11 @@ type GrafanaMetaAccessor interface { // Deprecated: This will be removed in Grafana 13 SetDeprecatedInternalID(id int64) + GetFullpath() string + SetFullpath(path string) + GetFullpathUIDs() string + SetFullpathUIDs(uids string) + GetSpec() (any, error) SetSpec(any) error @@ -317,6 +325,22 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) { m.obj.SetLabels(labels) } +func (m *grafanaMetaAccessor) GetFullpath() string { + return m.get(AnnoKeyFullpath) +} + +func (m *grafanaMetaAccessor) SetFullpath(path string) { + m.SetAnnotation(AnnoKeyFullpath, path) +} + +func (m *grafanaMetaAccessor) GetFullpathUIDs() string { + return m.get(AnnoKeyFullpathUIDs) +} + +func (m *grafanaMetaAccessor) SetFullpathUIDs(uids string) { + m.SetAnnotation(AnnoKeyFullpathUIDs, uids) +} + // GetAnnotations implements GrafanaMetaAccessor. func (m *grafanaMetaAccessor) GetAnnotations() map[string]string { return m.obj.GetAnnotations() diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 6caffa28022..3159e9acf68 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -72,6 +72,14 @@ func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) // We're going to have to align with that. For now we do need the user ID because the folder type stores it // as the only user identifier + if v.Fullpath != "" { + meta.SetFullpath(v.Fullpath) + } + + if v.FullpathUIDs != "" { + meta.SetFullpathUIDs(v.FullpathUIDs) + } + if v.CreatedBy != 0 { meta.SetCreatedBy(claims.NewTypeID(claims.TypeUser, strconv.FormatInt(v.CreatedBy, 10))) } diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index ac34b4c9683..86c27b49a94 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -7,6 +7,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" @@ -18,6 +19,7 @@ import ( "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/org" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -105,6 +107,12 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO paging.page = 1 } + // only admins can add this to the query, otherwise we may return parent folder names that are not visible to the user + if user.GetOrgRole() == org.RoleAdmin && options.LabelSelector != nil && options.LabelSelector.Matches(labels.Set{utils.AnnoKeyFullpath: "true"}) { + query.WithFullpath = true + query.WithFullpathUIDs = true + } + hits, err := s.service.GetFoldersLegacy(ctx, query) if err != nil { return nil, err diff --git a/pkg/registry/apis/folders/legacy_storage_test.go b/pkg/registry/apis/folders/legacy_storage_test.go index 824b04030c3..267dcd51b51 100644 --- a/pkg/registry/apis/folders/legacy_storage_test.go +++ b/pkg/registry/apis/folders/legacy_storage_test.go @@ -8,13 +8,16 @@ import ( "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/meta" metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/labels" "encoding/base64" "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/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" ) @@ -119,3 +122,75 @@ func TestLegacyStorage_List_Pagination(t *testing.T) { require.Equal(t, int64(1), folderService.LastQuery.Page) }) } + +func TestLegacyStorage_List_LabelSelector(t *testing.T) { + usr := &user.SignedInUser{UserID: 1, OrgRole: org.RoleAdmin} + ctx := identity.WithRequester(context.Background(), usr) + folderService := &foldertest.FakeService{} + storage := legacyStorage{ + service: folderService, + namespacer: func(_ int64) string { return "1" }, + } + + t.Run("should handle nil label selector", func(t *testing.T) { + options := &metainternalversion.ListOptions{ + LabelSelector: nil, + } + + folders := []*folder.Folder{ + { + UID: "folder-1", + Title: "Folder 1", + }, + } + folderService.ExpectedFolders = folders + + result, err := storage.List(ctx, options) + require.NoError(t, err) + + // verify we queried the service correctly + require.False(t, folderService.LastQuery.WithFullpath) + require.False(t, folderService.LastQuery.WithFullpathUIDs) + + list, ok := result.(*v0alpha1.FolderList) + require.True(t, ok) + require.Len(t, list.Items, 1) + }) + + t.Run("should set fullpath query parameters when label selector matches", func(t *testing.T) { + selector, err := labels.Parse(utils.AnnoKeyFullpath + "=true") + require.NoError(t, err) + options := &metainternalversion.ListOptions{ + LabelSelector: selector, + } + + folders := []*folder.Folder{ + { + UID: "folder-1", + Title: "Folder 1", + Fullpath: "/Folder 1", + FullpathUIDs: "/folder-1", + }, + } + folderService.ExpectedFolders = folders + + result, err := storage.List(ctx, options) + require.NoError(t, err) + + // verify we queried the service correctly + require.True(t, folderService.LastQuery.WithFullpath) + require.True(t, folderService.LastQuery.WithFullpathUIDs) + + list, ok := result.(*v0alpha1.FolderList) + require.True(t, ok) + require.Len(t, list.Items, 1) + + folder := list.Items[0] + meta, err := utils.MetaAccessor(&folder) + require.NoError(t, err) + + // make sure the annotations are set + require.Equal(t, "/Folder 1", meta.GetFullpath()) + require.Equal(t, "/folder-1", meta.GetFullpathUIDs()) + }) +} diff --git a/pkg/services/folder/folderimpl/conversions.go b/pkg/services/folder/folderimpl/conversions.go index 86c749556be..404a78b9e03 100644 --- a/pkg/services/folder/folderimpl/conversions.go +++ b/pkg/services/folder/folderimpl/conversions.go @@ -58,6 +58,7 @@ func (ss *FolderUnifiedStoreImpl) UnstructuredToLegacyFolder(ctx context.Context if updater.UID == "" { updater = creator } + manager, _ := meta.GetManagerProperties() return &folder.Folder{ UID: uid, @@ -68,12 +69,14 @@ func (ss *FolderUnifiedStoreImpl) UnstructuredToLegacyFolder(ctx context.Context Version: int(meta.GetGeneration()), ManagedBy: manager.Kind, - URL: url, - Created: created, - Updated: *updated, - OrgID: info.OrgID, - CreatedBy: creator.ID, - UpdatedBy: updater.ID, + Fullpath: meta.GetFullpath(), + FullpathUIDs: meta.GetFullpathUIDs(), + URL: url, + Created: created, + Updated: *updated, + OrgID: info.OrgID, + CreatedBy: creator.ID, + UpdatedBy: updater.ID, }, nil } diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 0d64cdcf2f3..57aa43cabb3 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -308,7 +308,14 @@ func (ss *FolderUnifiedStoreImpl) GetHeight(ctx context.Context, foldrUID string // The full path UIDs of B is "uid1/uid2". // The full path UIDs of A is "uid1". func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFoldersFromStoreQuery) ([]*folder.Folder, error) { - out, err := ss.k8sclient.List(ctx, q.OrgID, v1.ListOptions{}) + opts := v1.ListOptions{} + if q.WithFullpath || q.WithFullpathUIDs { + // only supported in modes 0-2, to keep the alerting queries from causing tons of get folder requests + // to retrieve the parent for all folders in grafana + opts.LabelSelector = utils.AnnoKeyFullpath + "=true" + } + + out, err := ss.k8sclient.List(ctx, q.OrgID, opts) if err != nil { return nil, err } @@ -320,7 +327,8 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo if f == nil { return nil, fmt.Errorf("unable to convert unstructured item to legacy folder %w", err) } - if q.WithFullpath || q.WithFullpathUIDs { + + if (q.WithFullpath || q.WithFullpathUIDs) && f.Fullpath == "" { parents, err := ss.GetParents(ctx, folder.GetParentsQuery{UID: f.UID, OrgID: q.OrgID}) if err != nil { return nil, fmt.Errorf("failed to get parents for folder %s: %w", f.UID, err)