Folders: Split legacy out of folder.Service (and remove folder.FolderStore) (#110734)

This commit is contained in:
Ryan McKinley
2025-09-08 18:27:49 +03:00
committed by GitHub
parent 854a8f7e70
commit 7c95d3c8a9
26 changed files with 99 additions and 332 deletions
@@ -17,7 +17,7 @@ type DashboardFolderStoreImpl struct {
store db.DB
}
func ProvideDashboardFolderStore(sqlStore db.DB) *DashboardFolderStoreImpl {
func newDashboardFolderStore(sqlStore db.DB) *DashboardFolderStoreImpl {
return &DashboardFolderStoreImpl{store: sqlStore}
}
@@ -44,7 +44,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) {
setup()
var orgId int64 = 1
sqlStore := db.InitTestDB(t)
folderStore := ProvideDashboardFolderStore(sqlStore)
folderStore := newDashboardFolderStore(sqlStore)
folder := insertTestFolder(t, dashboardStore, "TEST", orgId, "", "prod")
dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, folder.UID, "prod")
@@ -69,7 +69,7 @@ func TestIntegrationDashboardFolderStore(t *testing.T) {
setup()
var orgId int64 = 1
sqlStore := db.InitTestDB(t)
folderStore := ProvideDashboardFolderStore(sqlStore)
folderStore := newDashboardFolderStore(sqlStore)
folder := insertTestFolder(t, dashboardStore, "TEST", orgId, "", "prod")
dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.ID, folder.UID, "prod")
@@ -122,7 +122,7 @@ func TestIntegrationGetDashFolderStore(t *testing.T) {
folderStore := ProvideStore(db)
dashboardStore, err := database.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db))
require.NoError(t, err)
dashFolderStore := ProvideDashboardFolderStore(db)
dashFolderStore := newDashboardFolderStore(db)
orgID := CreateOrg(t, db, cfg)
+23 -48
View File
@@ -17,7 +17,6 @@ import (
"golang.org/x/exp/slices"
"github.com/grafana/dskit/concurrency"
dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -50,13 +49,18 @@ import (
const FULLPATH_SEPARATOR = "/"
var (
_ folder.LegacyService = (*Service)(nil)
_ folder.Service = (*Service)(nil)
)
type Service struct {
store folder.Store
unifiedStore folder.Store
db db.DB
log *slog.Logger
dashboardStore dashboards.Store
dashboardFolderStore folder.FolderStore
dashboardStore dashboards.Store // folders are saved in the dashboard table
dashboardFolderStore *DashboardFolderStoreImpl
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
k8sclient client.K8sHandler
@@ -77,7 +81,6 @@ func ProvideService(
ac accesscontrol.AccessControl,
bus bus.Bus,
dashboardStore dashboards.Store,
folderStore folder.FolderStore,
userService user.Service,
db db.DB, // DB for the (new) nested folder store
features featuremgmt.FeatureToggles,
@@ -94,7 +97,7 @@ func ProvideService(
srv := &Service{
log: slog.Default().With("logger", "folder-service"),
dashboardStore: dashboardStore,
dashboardFolderStore: folderStore,
dashboardFolderStore: newDashboardFolderStore(db),
store: store,
features: features,
accessControl: ac,
@@ -109,7 +112,7 @@ func ProvideService(
supportBundles.RegisterSupportItemCollector(srv.supportBundleCollector())
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(srv.getUIDFromLegacyID, srv))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv))
k8sHandler := client.NewK8sHandler(
@@ -194,6 +197,14 @@ func (s *Service) DBMigration(db db.DB) {
s.log.Debug("syncing dashboard and folder tables finished")
}
func (s *Service) getUIDFromLegacyID(ctx context.Context, orgID int64, id int64) (string, error) {
f, err := s.dashboardFolderStore.GetFolderByID(ctx, orgID, id)
if err != nil {
return "", err
}
return f.UID, nil
}
func (s *Service) CountFoldersInOrg(ctx context.Context, orgID int64) (int64, error) {
ctx, span := s.tracer.Start(ctx, "folder.CountFoldersInOrg")
defer span.End()
@@ -315,7 +326,7 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, forceLegacy
var parents []*folder.Folder
var err error
if forceLegacy {
parents, err = s.GetParentsLegacy(ctx, folder.GetParentsQuery{
parents, err = s.getParentsLegacy(ctx, folder.GetParentsQuery{
UID: f.UID,
OrgID: f.OrgID,
})
@@ -340,8 +351,8 @@ func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) (
return s.getChildrenFromApiServer(ctx, q)
}
func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetChildrenLegacy")
func (s *Service) getChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
ctx, span := s.tracer.Start(ctx, "folder.getChildrenLegacy")
defer span.End()
defer func(t time.Time) {
parent := q.UID
@@ -493,7 +504,7 @@ func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuer
}
var rootFolders []*folder.FolderReference
if forceLegacy {
rootFolders, err = s.GetChildrenLegacy(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
rootFolders, err = s.getChildrenLegacy(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
} else {
rootFolders, err = s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
}
@@ -620,8 +631,8 @@ func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*
return s.getParentsFromApiServer(ctx, q)
}
func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetParentsLegacy")
func (s *Service) getParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.getParentsLegacy")
defer span.End()
if q.UID == accesscontrol.GeneralFolderUID {
return nil, nil
@@ -1250,42 +1261,6 @@ func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescenda
return s.getDescendantCountsFromApiServer(ctx, q)
}
func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetDescendantCountsLegacy")
defer span.End()
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
}
if q.UID == nil || *q.UID == "" {
return nil, folder.ErrBadRequest.Errorf("missing UID")
}
if q.OrgID < 1 {
return nil, folder.ErrBadRequest.Errorf("invalid orgID")
}
folders := []string{*q.UID}
countsMap := make(folder.DescendantCounts, len(s.registry)+1)
descendantFolders, err := s.store.GetDescendants(ctx, q.OrgID, *q.UID)
if err != nil {
s.log.ErrorContext(ctx, "failed to get descendant folders", "error", err)
return nil, err
}
for _, f := range descendantFolders {
folders = append(folders, f.UID)
}
countsMap[entity.StandardKindFolder] = int64(len(descendantFolders))
for _, v := range s.registry {
c, err := v.CountInFolders(ctx, q.OrgID, folders, q.SignedInUser)
if err != nil {
s.log.ErrorContext(ctx, "failed to count folder descendants", "error", err)
return nil, err
}
countsMap[v.Kind()] = c
}
return countsMap, nil
}
// buildSaveDashboardCommand is a simplified version on DashboardServiceImpl.buildSaveDashboardCommand
// keeping only the meaningful functionality for folders
func (s *Service) buildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO) (*dashboards.SaveDashboardCommand, error) {
@@ -12,12 +12,8 @@ import (
"k8s.io/apimachinery/pkg/selection"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
folderv1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/infra/log"
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -26,6 +22,8 @@ import (
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/util"
)
@@ -1,149 +0,0 @@
// Code generated by mockery v2.53.4. DO NOT EDIT.
package foldertest
import (
context "context"
folder "github.com/grafana/grafana/pkg/services/folder"
mock "github.com/stretchr/testify/mock"
)
// FakeFolderStore is an autogenerated mock type for the FolderStore type
type FakeFolderStore struct {
mock.Mock
}
// Get provides a mock function with given fields: ctx, q
func (_m *FakeFolderStore) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) {
ret := _m.Called(ctx, q)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) (*folder.Folder, error)); ok {
return rf(ctx, q)
}
if rf, ok := ret.Get(0).(func(context.Context, folder.GetFolderQuery) *folder.Folder); ok {
r0 = rf(ctx, q)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, folder.GetFolderQuery) error); ok {
r1 = rf(ctx, q)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolderByID provides a mock function with given fields: ctx, orgID, id
func (_m *FakeFolderStore) GetFolderByID(ctx context.Context, orgID int64, id int64) (*folder.Folder, error) {
ret := _m.Called(ctx, orgID, id)
if len(ret) == 0 {
panic("no return value specified for GetFolderByID")
}
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) (*folder.Folder, error)); ok {
return rf(ctx, orgID, id)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) *folder.Folder); ok {
r0 = rf(ctx, orgID, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, int64) error); ok {
r1 = rf(ctx, orgID, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolderByUID provides a mock function with given fields: ctx, orgID, uid
func (_m *FakeFolderStore) GetFolderByUID(ctx context.Context, orgID int64, uid string) (*folder.Folder, error) {
ret := _m.Called(ctx, orgID, uid)
if len(ret) == 0 {
panic("no return value specified for GetFolderByUID")
}
var r0 *folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, string) (*folder.Folder, error)); ok {
return rf(ctx, orgID, uid)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, string) *folder.Folder); ok {
r0 = rf(ctx, orgID, uid)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok {
r1 = rf(ctx, orgID, uid)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFolders provides a mock function with given fields: ctx, orgID, uids
func (_m *FakeFolderStore) GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*folder.Folder, error) {
ret := _m.Called(ctx, orgID, uids)
if len(ret) == 0 {
panic("no return value specified for GetFolders")
}
var r0 map[string]*folder.Folder
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64, []string) (map[string]*folder.Folder, error)); ok {
return rf(ctx, orgID, uids)
}
if rf, ok := ret.Get(0).(func(context.Context, int64, []string) map[string]*folder.Folder); ok {
r0 = rf(ctx, orgID, uids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]*folder.Folder)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64, []string) error); ok {
r1 = rf(ctx, orgID, uids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewFakeFolderStore creates a new instance of FakeFolderStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewFakeFolderStore(t interface {
mock.TestingT
Cleanup(func())
}) *FakeFolderStore {
mock := &FakeFolderStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
@@ -59,17 +59,9 @@ func (s *FakeService) GetChildren(ctx context.Context, q *folder.GetChildrenQuer
}
return result, nil
}
func (s *FakeService) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
return s.ExpectedFoldersRef, s.ExpectedError
}
func (s *FakeService) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
return s.ExpectedFolders, s.ExpectedError
}
func (s *FakeService) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
return s.ExpectedFolders, s.ExpectedError
}
func (s *FakeService) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
@@ -117,9 +109,6 @@ func (s *FakeService) RegisterService(service folder.RegistryService) error {
func (s *FakeService) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
return s.ExpectedDescendantCounts, s.ExpectedError
}
func (s *FakeService) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
return s.ExpectedDescendantCounts, s.ExpectedError
}
func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
if s.foldersByUID != nil && len(q.UIDs) > 0 {
+10 -23
View File
@@ -6,11 +6,20 @@ import (
"github.com/grafana/grafana/pkg/services/search/model"
)
type LegacyService interface {
CreateLegacy(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error)
GetLegacy(ctx context.Context, q *GetFolderQuery) (*Folder, error)
UpdateLegacy(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
DeleteLegacy(ctx context.Context, cmd *DeleteFolderCommand) error
MoveLegacy(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
GetFoldersLegacy(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
}
// The folder.Service is backed by calls forwarding to an apiserver
type Service interface {
RegisterService(service RegistryService) error
Create(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error)
CreateLegacy(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error)
// GetFolder takes a GetFolderCommand and returns a folder matching the
// request. One of UID, ID or Title must be included. If multiple values
@@ -20,19 +29,15 @@ type Service interface {
// If ParentUID is not set then the folder will be fetched from the root level.
// If WithFullpath is true it computes also the full path of a folder.
Get(ctx context.Context, q *GetFolderQuery) (*Folder, error)
GetLegacy(ctx context.Context, q *GetFolderQuery) (*Folder, error)
// Update is used to update a folder's UID, Title and Description. To change
// a folder's parent folder, use Move.
Update(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
UpdateLegacy(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
Delete(ctx context.Context, cmd *DeleteFolderCommand) error
DeleteLegacy(ctx context.Context, cmd *DeleteFolderCommand) error
// Move changes a folder's parent folder to the requested new parent.
Move(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
MoveLegacy(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
// GetFolders returns org folders that are accessible by the signed in user by their UIDs.
// If WithFullpath is true it computes also the full path of a folder.
@@ -40,36 +45,18 @@ type Service interface {
// If a folder contains a slash in its title, it is escaped with a backslash.
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
GetFolders(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
GetFoldersLegacy(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
// SearchFolders returns a list of folders that match the query.
SearchFolders(ctx context.Context, q SearchFoldersQuery) (model.HitList, error)
// GetChildren returns an array containing all child folders.
GetChildren(ctx context.Context, q *GetChildrenQuery) ([]*FolderReference, error)
GetChildrenLegacy(ctx context.Context, q *GetChildrenQuery) ([]*FolderReference, error)
// GetParents returns an array containing add parent folders if nested folders are enabled
// otherwise it returns an empty array
GetParents(ctx context.Context, q GetParentsQuery) ([]*Folder, error)
GetParentsLegacy(ctx context.Context, q GetParentsQuery) ([]*Folder, error)
GetDescendantCounts(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
GetDescendantCountsLegacy(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
CountFoldersInOrg(ctx context.Context, orgID int64) (int64, error)
}
// FolderStore is a folder store.
//
//go:generate mockery --name FolderStore --structname FakeFolderStore --outpkg foldertest --output foldertest --filename folder_store_mock.go
type FolderStore interface {
// Get joins on the dashboard and folder table to return all information needed for a folder
Get(ctx context.Context, q GetFolderQuery) (*Folder, error)
// GetFolderByUID retrieves a folder by its UID
GetFolderByUID(ctx context.Context, orgID int64, uid string) (*Folder, error)
// GetFolderByID retrieves a folder by its ID
GetFolderByID(ctx context.Context, orgID int64, id int64) (*Folder, error)
// GetFolders returns all folders for the given orgID and UIDs.
GetFolders(ctx context.Context, orgID int64, uids []string) (map[string]*Folder, error)
}