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:
maicon 2025-01-13 18:15:35 -03:00 committed by GitHub
parent 009d7f42b3
commit 766d645d82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1900 additions and 73 deletions

View File

@ -173,6 +173,7 @@ Experimental features might be changed or removed without prior notice.
| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally |
| `kubernetesRestore` | Allow restoring objects in k8s |
| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s |
| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s |
| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs |
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query |

View File

@ -113,6 +113,7 @@ export interface FeatureToggles {
kubernetesCliDashboards?: boolean;
kubernetesRestore?: boolean;
kubernetesFolders?: boolean;
kubernetesFoldersServiceV2?: boolean;
grafanaAPIServerTestingWithExperimentalAPIs?: boolean;
datasourceQueryTypes?: boolean;
queryService?: boolean;

View File

@ -834,7 +834,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
quotaService := quotatest.New(false, nil)
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
dashboardStore, folderStore, db, features,
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
if dashboardService == nil {
dashboardService, err = service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,

View File

@ -57,7 +57,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
})
})
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) && !hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
// Use k8s client to implement legacy API
handler := newFolderK8sHandler(hs)
folderRoute.Post("/", handler.createFolder)

View File

@ -461,7 +461,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
actionSets := resourcepermissions.NewActionSetService(features)
fStore := folderimpl.ProvideStore(sc.db)
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
acSvc := acimpl.ProvideOSSService(
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn,

View File

@ -52,6 +52,11 @@ func LegacyUpdateCommandToUnstructured(obj *unstructured.Unstructured, cmd *fold
if cmd.NewDescription != nil {
spec["description"] = cmd.NewDescription
}
if cmd.NewParentUID != nil {
if err := setParentUID(obj, *cmd.NewParentUID); err != nil {
return &unstructured.Unstructured{}, err
}
}
return obj, nil
}

View File

@ -97,7 +97,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
}
// List must return all folders
hits, err := s.service.GetFolders(ctx, folder.GetFoldersQuery{
hits, err := s.service.GetFoldersLegacy(ctx, folder.GetFoldersQuery{
SignedInUser: user,
OrgID: orgId,
// TODO: enable pagination
@ -133,7 +133,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
return nil, err
}
dto, err := s.service.Get(ctx, &folder.GetFolderQuery{
dto, err := s.service.GetLegacy(ctx, &folder.GetFolderQuery{
SignedInUser: user,
UID: &name,
OrgID: info.OrgID,
@ -186,7 +186,7 @@ func (s *legacyStorage) Create(ctx context.Context,
parent := accessor.GetFolder()
out, err := s.service.Create(ctx, &folder.CreateFolderCommand{
out, err := s.service.CreateLegacy(ctx, &folder.CreateFolderCommand{
SignedInUser: user,
UID: p.Name,
Title: p.Spec.Title,
@ -285,7 +285,7 @@ func (s *legacyStorage) Update(ctx context.Context,
oldParent := mOld.GetFolder()
newParent := mNew.GetFolder()
if oldParent != newParent {
_, err = s.service.Move(ctx, &folder.MoveFolderCommand{
_, err = s.service.MoveLegacy(ctx, &folder.MoveFolderCommand{
SignedInUser: user,
UID: name,
OrgID: info.OrgID,
@ -312,7 +312,7 @@ func (s *legacyStorage) Update(ctx context.Context,
changed = true
}
if changed {
_, err = s.service.Update(ctx, cmd)
_, err = s.service.UpdateLegacy(ctx, cmd)
if err != nil {
return nil, false, err
}
@ -340,7 +340,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
if !ok {
return v, false, fmt.Errorf("expected a folder response from Get")
}
err = s.service.Delete(ctx, &folder.DeleteFolderCommand{
err = s.service.DeleteLegacy(ctx, &folder.DeleteFolderCommand{
UID: name,
OrgID: info.OrgID,
SignedInUser: user,

View File

@ -66,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg,
) *FolderAPIBuilder {
if !featuremgmt.AnyEnabled(features,
featuremgmt.FlagKubernetesFolders,
featuremgmt.FlagKubernetesFoldersServiceV2,
featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs,
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagProvisioning) {

View File

@ -49,7 +49,7 @@ func ProvideFolderPermissions(
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
dashboardStore, folderStore, sqlStore, features,
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
acSvc := acimpl.ProvideOSSService(
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),

View File

@ -50,7 +50,7 @@ func TestIntegrationAuthorize(t *testing.T) {
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
dashStore, folderStore, sql, featuremgmt.WithFeatures(),
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
require.NoError(t, err)

View File

@ -62,7 +62,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
dashStore, folderStore, sql, featuremgmt.WithFeatures(),
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
require.NoError(t, err)
@ -244,7 +244,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
fStore := folderimpl.ProvideStore(sql)
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
folderStore, sql, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderStore, sql, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
require.NoError(t, err)

View File

@ -300,7 +300,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
})
folderStore := folderimpl.ProvideStore(sqlStore)
folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
parentUID := ""
for i := 0; ; i++ {

View File

@ -891,7 +891,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
user := &user.SignedInUser{
OrgID: 1,
@ -1010,7 +1010,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
fStore := folderimpl.ProvideStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
user := &user.SignedInUser{
OrgID: 1,

View File

@ -372,13 +372,15 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
// Validate folder
if dash.FolderUID != "" {
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
if err != nil {
return nil, err
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
if err != nil {
return nil, err
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck
dash.FolderID = folder.ID
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck
dash.FolderID = folder.ID
} else if dash.FolderID != 0 { // nolint:staticcheck
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck

View File

@ -101,6 +101,7 @@ func TestIntegrationDashboardServiceZanzana(t *testing.T) {
db,
featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
supportbundlestest.NewFakeBundleService(),
cfg,
nil,
tracing.InitializeTracerForTest(),
)

View File

@ -714,6 +714,12 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
},
{
Name: "kubernetesFoldersServiceV2",
Description: "Use the Folders Service V2, and route Folder Service requests to k8s",
Stage: FeatureStageExperimental,
Owner: grafanaSearchAndStorageSquad,
},
{
Name: "grafanaAPIServerTestingWithExperimentalAPIs",
Description: "Facilitate integration testing of experimental APIs",

View File

@ -94,6 +94,7 @@ kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,fals
kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false
kubernetesRestore,experimental,@grafana/grafana-app-platform-squad,false,false,false
kubernetesFolders,experimental,@grafana/search-and-storage,false,false,false
kubernetesFoldersServiceV2,experimental,@grafana/search-and-storage,false,false,false
grafanaAPIServerTestingWithExperimentalAPIs,experimental,@grafana/search-and-storage,false,false,false
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
94 kubernetesCliDashboards experimental @grafana/grafana-app-platform-squad false false false
95 kubernetesRestore experimental @grafana/grafana-app-platform-squad false false false
96 kubernetesFolders experimental @grafana/search-and-storage false false false
97 kubernetesFoldersServiceV2 experimental @grafana/search-and-storage false false false
98 grafanaAPIServerTestingWithExperimentalAPIs experimental @grafana/search-and-storage false false false
99 datasourceQueryTypes experimental @grafana/grafana-app-platform-squad false true false
100 queryService experimental @grafana/grafana-app-platform-squad false true false

View File

@ -387,6 +387,10 @@ const (
// Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s
FlagKubernetesFolders = "kubernetesFolders"
// FlagKubernetesFoldersServiceV2
// Use the Folders Service V2, and route Folder Service requests to k8s
FlagKubernetesFoldersServiceV2 = "kubernetesFoldersServiceV2"
// FlagGrafanaAPIServerTestingWithExperimentalAPIs
// Facilitate integration testing of experimental APIs
FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs"

View File

@ -2081,6 +2081,18 @@
"codeowner": "@grafana/search-and-storage"
}
},
{
"metadata": {
"name": "kubernetesFoldersServiceV2",
"resourceVersion": "1735336477446",
"creationTimestamp": "2024-12-27T21:54:37Z"
},
"spec": {
"description": "Use the Folders Service V2, and route Folder Service requests to k8s",
"stage": "experimental",
"codeowner": "@grafana/search-and-storage"
}
},
{
"metadata": {
"name": "kubernetesPlaylists",

View File

@ -18,12 +18,14 @@ import (
"golang.org/x/exp/slices"
"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/events"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -34,6 +36,7 @@ import (
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@ -41,12 +44,14 @@ const FULLPATH_SEPARATOR = "/"
type Service struct {
store folder.Store
unifiedStore folder.Store
db db.DB
log *slog.Logger
dashboardStore dashboards.Store
dashboardFolderStore folder.FolderStore
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
k8sclient folderK8sHandler
// bus is currently used to publish event in case of folder full path change.
// For example when a folder is moved to another folder or when a folder is renamed.
bus bus.Bus
@ -66,14 +71,24 @@ func ProvideService(
db db.DB, // DB for the (new) nested folder store
features featuremgmt.FeatureToggles,
supportBundles supportbundles.Service,
cfg *setting.Cfg,
r prometheus.Registerer,
tracer tracing.Tracer,
) *Service {
k8sHandler := &foldk8sHandler{
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
namespacer: request.GetNamespaceMapper(cfg),
cfg: cfg,
}
unifiedStore := ProvideUnifiedStore(cfg)
srv := &Service{
log: slog.Default().With("logger", "folder-service"),
dashboardStore: dashboardStore,
dashboardFolderStore: folderStore,
store: store,
unifiedStore: unifiedStore,
features: features,
accessControl: ac,
bus: bus,
@ -81,6 +96,7 @@ func ProvideService(
registry: make(map[string]folder.RegistryService),
metrics: newFoldersMetrics(r),
tracer: tracer,
k8sclient: k8sHandler,
}
srv.DBMigration(db)
@ -138,6 +154,13 @@ func (s *Service) DBMigration(db db.DB) {
}
func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.getFoldersFromApiServer(ctx, q)
}
return s.GetFoldersLegacy(ctx, q)
}
func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
@ -190,6 +213,13 @@ func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*
}
func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.getFromApiServer(ctx, q)
}
return s.GetLegacy(ctx, q)
}
func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
@ -278,13 +308,13 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo
}
if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) {
f, err = s.setFullpath(ctx, f, q.SignedInUser)
f, err = s.setFullpath(ctx, f, q.SignedInUser, true)
}
return f, err
}
func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identity.Requester) (*folder.Folder, error) {
func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identity.Requester, forceLegacy bool) (*folder.Folder, error) {
// #TODO is some kind of intermediate conversion required as is the case with user id where
// it gets parsed using UserIdentifier(). Also is there some kind of validation taking place as
// part of the parsing?
@ -297,10 +327,19 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identi
// Fetch the parent since the permissions for fetching the newly created folder
// are not yet present for the user--this requires a call to ClearUserPermissionCache
parents, err := s.GetParents(ctx, folder.GetParentsQuery{
UID: f.UID,
OrgID: f.OrgID,
})
var parents []*folder.Folder
var err error
if forceLegacy {
parents, err = s.GetParentsLegacy(ctx, folder.GetParentsQuery{
UID: f.UID,
OrgID: f.OrgID,
})
} else {
parents, err = s.GetParents(ctx, folder.GetParentsQuery{
UID: f.UID,
OrgID: f.OrgID,
})
}
if err != nil {
return nil, err
}
@ -319,6 +358,13 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identi
}
func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.getChildrenFromApiServer(ctx, q)
}
return s.GetChildrenLegacy(ctx, q)
}
func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
defer func(t time.Time) {
parent := q.UID
if q.UID != folder.SharedWithMeFolderUID {
@ -332,7 +378,7 @@ func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) (
}
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && q.UID == folder.SharedWithMeFolderUID {
return s.GetSharedWithMe(ctx, q)
return s.GetSharedWithMe(ctx, q, true)
}
if q.UID == "" {
@ -460,14 +506,19 @@ func (s *Service) getRootFolders(ctx context.Context, q *folder.GetChildrenQuery
}
// GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders
func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuery, forceLegacy bool) ([]*folder.Folder, error) {
start := time.Now()
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q)
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q, forceLegacy)
if err != nil {
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders to which the user has explicit access: %w", err)
}
rootFolders, err := s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
var rootFolders []*folder.Folder
if forceLegacy {
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})
}
if err != nil {
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
return nil, folder.ErrInternal.Errorf("failed to fetch root folders to which the user has access: %w", err)
@ -478,7 +529,7 @@ func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuer
return availableNonRootFolders, nil
}
func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetChildrenQuery, forceLegacy bool) ([]*folder.Folder, error) {
permissions := q.SignedInUser.GetPermissions()
var folderPermissions []string
if q.Permission == dashboardaccess.PERMISSION_EDIT {
@ -507,13 +558,25 @@ func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetC
return nonRootFolders, nil
}
dashFolders, err := s.GetFolders(ctx, folder.GetFoldersQuery{
UIDs: folderUids,
OrgID: q.OrgID,
SignedInUser: q.SignedInUser,
OrderByTitle: true,
WithFullpathUIDs: true,
})
var dashFolders []*folder.Folder
var err error
if forceLegacy {
dashFolders, err = s.GetFoldersLegacy(ctx, folder.GetFoldersQuery{
UIDs: folderUids,
OrgID: q.OrgID,
SignedInUser: q.SignedInUser,
OrderByTitle: true,
WithFullpathUIDs: true,
})
} else {
dashFolders, err = s.GetFolders(ctx, folder.GetFoldersQuery{
UIDs: folderUids,
OrgID: q.OrgID,
SignedInUser: q.SignedInUser,
OrderByTitle: true,
WithFullpathUIDs: true,
})
}
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
}
@ -565,6 +628,13 @@ func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*fo
}
func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.getParentsFromApiServer(ctx, q)
}
return s.GetParentsLegacy(ctx, q)
}
func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) || q.UID == accesscontrol.GeneralFolderUID {
return nil, nil
}
@ -591,6 +661,13 @@ func (s *Service) getFolderByTitle(ctx context.Context, orgID int64, title strin
}
func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.createOnApiServer(ctx, cmd)
}
return s.CreateLegacy(ctx, cmd)
}
func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
if cmd.SignedInUser == nil || cmd.SignedInUser.IsNil() {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
@ -700,13 +777,23 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (
}
if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) {
f, err = s.setFullpath(ctx, f, user)
f, err = s.setFullpath(ctx, f, user, true)
if err != nil {
return nil, err
}
}
return f, nil
}
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.updateOnApiServer(ctx, cmd)
}
return s.UpdateLegacy(ctx, cmd)
}
func (s *Service) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Update")
defer span.End()
@ -839,6 +926,13 @@ func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int6
}
func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.deleteFromApiServer(ctx, cmd)
}
return s.DeleteLegacy(ctx, cmd)
}
func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
if cmd.SignedInUser == nil {
return folder.ErrBadRequest.Errorf("missing signed in user")
}
@ -922,6 +1016,13 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
}
func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.moveOnApiServer(ctx, cmd)
}
return s.MoveLegacy(ctx, cmd)
}
func (s *Service) MoveLegacy(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Move")
defer span.End()
@ -1142,6 +1243,14 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold
}
func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
return s.getDescendantCountsFromApiServer(ctx, q)
}
return s.GetDescendantCountsLegacy(ctx, q)
}
func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
}

View File

@ -62,10 +62,10 @@ func TestIntegrationProvideFolderService(t *testing.T) {
}
t.Run("should register scope resolvers", func(t *testing.T) {
ac := acmock.New()
db, _ := db.InitTestDBWithCfg(t)
db, cfg := db.InitTestDBWithCfg(t)
store := ProvideStore(db)
ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db,
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
})

View File

@ -0,0 +1,702 @@
package folderimpl
import (
"context"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/exp/slices"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
// interface to allow for testing
type folderK8sHandler interface {
getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
getNamespace(orgID int64) string
}
var _ folderK8sHandler = (*foldk8sHandler)(nil)
type foldk8sHandler struct {
cfg *setting.Cfg
namespacer request.NamespaceMapper
gvr schema.GroupVersionResource
}
func (s *Service) getFoldersFromApiServer(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
qry := folder.NewGetFoldersQuery(q)
permissions := q.SignedInUser.GetPermissions()
folderPermissions := permissions[dashboards.ActionFoldersRead]
qry.AncestorUIDs = make([]string, 0, len(folderPermissions))
if len(folderPermissions) == 0 && !q.SignedInUser.GetIsGrafanaAdmin() {
return nil, nil
}
for _, p := range folderPermissions {
if p == dashboards.ScopeFoldersAll {
// no need to query for folders with permissions
// the user has permission to access all folders
qry.AncestorUIDs = nil
break
}
if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found {
if !slices.Contains(qry.AncestorUIDs, folderUid) {
qry.AncestorUIDs = append(qry.AncestorUIDs, folderUid)
}
}
}
var dashFolders []*folder.Folder
var err error
ctx = identity.WithRequester(ctx, q.SignedInUser)
dashFolders, err = s.unifiedStore.GetFolders(ctx, qry)
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
}
return dashFolders, nil
}
func (s *Service) getFromApiServer(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
if q.UID != nil && *q.UID == accesscontrol.GeneralFolderUID {
return folder.RootFolder, nil
}
if q.UID != nil && *q.UID == folder.SharedWithMeFolderUID {
return folder.SharedWithMeFolder.WithURL(), nil
}
var dashFolder *folder.Folder
var err error
switch {
case q.UID != nil:
if *q.UID == "" {
return &folder.GeneralFolder, nil
}
dashFolder, err = s.unifiedStore.Get(ctx, *q)
if err != nil {
return nil, err
}
// nolint:staticcheck
case q.ID != nil:
// not implemented
case q.Title != nil:
// not implemented
default:
return nil, folder.ErrBadRequest.Errorf("either on of UID, ID, Title fields must be present")
}
if dashFolder.IsGeneral() {
return dashFolder, nil
}
// do not get guardian by the folder ID because it differs from the nested folder ID
// and the legacy folder ID has been associated with the permissions:
// use the folde UID instead that is the same for both
g, err := guardian.NewByFolder(ctx, dashFolder, dashFolder.OrgID, q.SignedInUser)
if err != nil {
return nil, err
}
if canView, err := g.CanView(); err != nil || !canView {
if err != nil {
return nil, toFolderError(err)
}
return nil, dashboards.ErrFolderAccessDenied
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
if q.ID != nil {
q.ID = nil
q.UID = &dashFolder.UID
}
f := dashFolder
// always expose the dashboard store sequential ID
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
// nolint:staticcheck
f.ID = dashFolder.ID
f.Version = dashFolder.Version
f, err = s.setFullpath(ctx, f, q.SignedInUser, false)
if err != nil {
return nil, err
}
return f, err
}
func (s *Service) getChildrenFromApiServer(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
defer func(t time.Time) {
parent := q.UID
if q.UID != folder.SharedWithMeFolderUID {
parent = "folder"
}
s.metrics.foldersGetChildrenRequestsDuration.WithLabelValues(parent).Observe(time.Since(t).Seconds())
}(time.Now())
if q.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
if q.UID == folder.SharedWithMeFolderUID {
return s.GetSharedWithMe(ctx, q, false)
}
if q.UID == "" {
return s.getRootFoldersFromApiServer(ctx, q)
}
var err error
// TODO: figure out what to do with Guardian
// we only need to check access to the folder
// if the parent is accessible then the subfolders are accessible as well (due to inheritance)
f := &folder.Folder{
UID: q.UID,
}
g, err := guardian.NewByFolder(ctx, f, q.OrgID, q.SignedInUser)
if err != nil {
return nil, err
}
guardianFunc := g.CanView
if q.Permission == dashboardaccess.PERMISSION_EDIT {
guardianFunc = g.CanEdit
}
hasAccess, err := guardianFunc()
if err != nil {
return nil, err
}
if !hasAccess {
return nil, dashboards.ErrFolderAccessDenied
}
children, err := s.unifiedStore.GetChildren(ctx, *q)
if err != nil {
return nil, err
}
return children, nil
}
func (s *Service) getRootFoldersFromApiServer(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
permissions := q.SignedInUser.GetPermissions()
var folderPermissions []string
if q.Permission == dashboardaccess.PERMISSION_EDIT {
folderPermissions = permissions[dashboards.ActionFoldersWrite]
} else {
folderPermissions = permissions[dashboards.ActionFoldersRead]
}
if len(folderPermissions) == 0 && !q.SignedInUser.GetIsGrafanaAdmin() {
return nil, nil
}
q.FolderUIDs = make([]string, 0, len(folderPermissions))
for _, p := range folderPermissions {
if p == dashboards.ScopeFoldersAll {
// no need to query for folders with permissions
// the user has permission to access all folders
q.FolderUIDs = nil
break
}
if folderUid, found := strings.CutPrefix(p, dashboards.ScopeFoldersPrefix); found {
if !slices.Contains(q.FolderUIDs, folderUid) {
q.FolderUIDs = append(q.FolderUIDs, folderUid)
}
}
}
children, err := s.unifiedStore.GetChildren(ctx, *q)
if err != nil {
return nil, err
}
// add "shared with me" folder on the 1st page
if (q.Page == 0 || q.Page == 1) && len(q.FolderUIDs) != 0 {
children = append([]*folder.Folder{&folder.SharedWithMeFolder}, children...)
}
return children, nil
}
func (s *Service) getParentsFromApiServer(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
if q.UID == accesscontrol.GeneralFolderUID {
return nil, nil
}
if q.UID == folder.SharedWithMeFolderUID {
return []*folder.Folder{&folder.SharedWithMeFolder}, nil
}
return s.unifiedStore.GetParents(ctx, q)
}
func (s *Service) createOnApiServer(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
if cmd.SignedInUser == nil || cmd.SignedInUser.IsNil() {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
if cmd.ParentUID != "" {
// Check that the user is allowed to create a subfolder in this folder
parentUIDScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.ParentUID)
legacyEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, parentUIDScope)
newEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, parentUIDScope)
evaluator := accesscontrol.EvalAny(legacyEvaluator, newEvaluator)
hasAccess, evalErr := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator)
if evalErr != nil {
return nil, evalErr
}
if !hasAccess {
return nil, dashboards.ErrFolderCreationAccessDenied.Errorf("user is missing the permission with action either folders:create or folders:write and scope %s or any of the parent folder scopes", parentUIDScope)
}
} else {
evaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
hasAccess, evalErr := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator)
if evalErr != nil {
return nil, evalErr
}
if !hasAccess {
return nil, dashboards.ErrFolderCreationAccessDenied.Errorf("user is missing the permission with action folders:create and scope folders:uid:general, which is required to create a folder under the root level")
}
}
if cmd.UID == folder.SharedWithMeFolderUID {
return nil, folder.ErrBadRequest.Errorf("cannot create folder with UID %s", folder.SharedWithMeFolderUID)
}
trimmedUID := strings.TrimSpace(cmd.UID)
if trimmedUID == accesscontrol.GeneralFolderUID {
return nil, dashboards.ErrFolderInvalidUID
}
user := cmd.SignedInUser
cmd = &folder.CreateFolderCommand{
// TODO: Today, if a UID isn't specified, the dashboard store
// generates a new UID. The new folder store will need to do this as
// well, but for now we take the UID from the newly created folder.
UID: trimmedUID,
OrgID: cmd.OrgID,
Title: cmd.Title,
Description: cmd.Description,
ParentUID: cmd.ParentUID,
SignedInUser: cmd.SignedInUser,
}
f, err := s.unifiedStore.Create(ctx, *cmd)
if err != nil {
return nil, err
}
f, err = s.setFullpath(ctx, f, user, false)
if err != nil {
return nil, err
}
return f, nil
}
func (s *Service) updateOnApiServer(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Update")
defer span.End()
if cmd.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
if cmd.NewTitle != nil && *cmd.NewTitle != "" {
title := strings.TrimSpace(*cmd.NewTitle)
cmd.NewTitle = &title
if strings.EqualFold(*cmd.NewTitle, dashboards.RootFolderName) {
return nil, dashboards.ErrDashboardFolderNameExists
}
}
if !util.IsValidShortUID(cmd.UID) {
return nil, dashboards.ErrDashboardInvalidUid
} else if util.IsShortUIDTooLong(cmd.UID) {
return nil, dashboards.ErrDashboardUidTooLong
}
cmd.UID = strings.TrimSpace(cmd.UID)
if cmd.NewTitle != nil && *cmd.NewTitle == "" {
return nil, dashboards.ErrDashboardTitleEmpty
}
f := &folder.Folder{
UID: cmd.UID,
}
g, err := guardian.NewByFolder(ctx, f, cmd.OrgID, cmd.SignedInUser)
if err != nil {
return nil, err
}
if canSave, err := g.CanSave(); err != nil || !canSave {
if err != nil {
return nil, err
}
return nil, toFolderError(dashboards.ErrDashboardUpdateAccessDenied)
}
user := cmd.SignedInUser
foldr, err := s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{
UID: cmd.UID,
OrgID: cmd.OrgID,
NewTitle: cmd.NewTitle,
NewDescription: cmd.NewDescription,
SignedInUser: user,
})
if err != nil {
return nil, err
}
if cmd.NewTitle != nil {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
if err := s.publishFolderFullPathUpdatedEventViaApiServer(ctx, foldr.Updated, cmd.OrgID, cmd.UID); err != nil {
return nil, err
}
}
// always expose the dashboard store sequential ID
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc()
return foldr, nil
}
func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
if cmd.SignedInUser == nil {
return folder.ErrBadRequest.Errorf("missing signed in user")
}
if cmd.UID == "" {
return folder.ErrBadRequest.Errorf("missing UID")
}
if cmd.OrgID < 1 {
return folder.ErrBadRequest.Errorf("invalid orgID")
}
f := &folder.Folder{
UID: cmd.UID,
}
guard, err := guardian.NewByFolder(ctx, f, cmd.OrgID, cmd.SignedInUser)
if err != nil {
return err
}
if canSave, err := guard.CanDelete(); err != nil || !canSave {
if err != nil {
return toFolderError(err)
}
return dashboards.ErrFolderAccessDenied
}
descFolders, err := s.unifiedStore.GetDescendants(ctx, cmd.OrgID, cmd.UID)
if err != nil {
return err
}
folders := []string{cmd.UID}
for _, f := range descFolders {
folders = append(folders, f.UID)
}
if cmd.ForceDeleteRules {
if err := s.deleteChildrenInFolder(ctx, cmd.OrgID, folders, cmd.SignedInUser); err != nil {
return err
}
} else {
alertRuleSrv, ok := s.registry[entity.StandardKindAlertRule]
if !ok {
return folder.ErrInternal.Errorf("no alert rule service found in registry")
}
alertRulesInFolder, err := alertRuleSrv.CountInFolders(ctx, cmd.OrgID, folders, cmd.SignedInUser)
if err != nil {
s.log.Error("failed to count alert rules in folder", "error", err)
return err
}
if alertRulesInFolder > 0 {
return folder.ErrFolderNotEmpty.Errorf("folder contains %d alert rules", alertRulesInFolder)
}
}
err = s.unifiedStore.Delete(ctx, folders, cmd.OrgID)
if err != nil {
return err
}
return nil
}
func (s *Service) moveOnApiServer(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Move")
defer span.End()
if cmd.SignedInUser == nil {
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
}
// k6-specific check to prevent folder move for a k6-app folder and its children
if cmd.UID == accesscontrol.K6FolderUID {
return nil, folder.ErrBadRequest.Errorf("k6 project may not be moved")
}
f, err := s.unifiedStore.Get(ctx, folder.GetFolderQuery{
UID: &cmd.UID,
OrgID: cmd.OrgID,
SignedInUser: cmd.SignedInUser,
})
if err != nil {
return nil, err
}
if f != nil && f.ParentUID == accesscontrol.K6FolderUID {
return nil, folder.ErrBadRequest.Errorf("k6 project may not be moved")
}
// Check that the user is allowed to move the folder to the destination folder
hasAccess, evalErr := s.canMoveViaApiServer(ctx, cmd)
if evalErr != nil {
return nil, evalErr
}
if !hasAccess {
return nil, dashboards.ErrFolderAccessDenied
}
// here we get the folder, we need to get the height of current folder
// and the depth of the new parent folder, the sum can't bypass 8
folderHeight, err := s.unifiedStore.GetHeight(ctx, cmd.UID, cmd.OrgID, &cmd.NewParentUID)
if err != nil {
return nil, err
}
parents, err := s.unifiedStore.GetParents(ctx, folder.GetParentsQuery{UID: cmd.NewParentUID, OrgID: cmd.OrgID})
if err != nil {
return nil, err
}
// height of the folder that is being moved + this current folder itself + depth of the NewParent folder should be less than or equal MaxNestedFolderDepth
if folderHeight+len(parents)+1 > folder.MaxNestedFolderDepth {
return nil, folder.ErrMaximumDepthReached.Errorf("failed to move folder")
}
for _, parent := range parents {
// if the current folder is already a parent of newparent, we should return error
if parent.UID == cmd.UID {
return nil, folder.ErrCircularReference.Errorf("failed to move folder")
}
}
f, err = s.unifiedStore.Update(ctx, folder.UpdateFolderCommand{
UID: cmd.UID,
OrgID: cmd.OrgID,
NewParentUID: &cmd.NewParentUID,
SignedInUser: cmd.SignedInUser,
})
if err != nil {
return nil, folder.ErrInternal.Errorf("failed to move folder: %w", err)
}
if err := s.publishFolderFullPathUpdatedEventViaApiServer(ctx, f.Updated, cmd.OrgID, cmd.UID); err != nil {
return nil, err
}
return f, nil
}
func (s *Service) publishFolderFullPathUpdatedEventViaApiServer(ctx context.Context, timestamp time.Time, orgID int64, folderUID string) error {
ctx, span := s.tracer.Start(ctx, "folder.publishFolderFullPathUpdatedEventViaApiServer")
defer span.End()
descFolders, err := s.unifiedStore.GetDescendants(ctx, orgID, folderUID)
if err != nil {
s.log.ErrorContext(ctx, "Failed to get descendants of the folder", "folderUID", folderUID, "orgID", orgID, "error", err)
return err
}
uids := make([]string, 0, len(descFolders)+1)
uids = append(uids, folderUID)
for _, f := range descFolders {
uids = append(uids, f.UID)
}
span.AddEvent("found folder descendants", trace.WithAttributes(
attribute.Int64("folders", int64(len(uids))),
))
if err := s.bus.Publish(ctx, &events.FolderFullPathUpdated{
Timestamp: timestamp,
UIDs: uids,
OrgID: orgID,
}); err != nil {
s.log.ErrorContext(ctx, "Failed to publish FolderFullPathUpdated event", "folderUID", folderUID, "orgID", orgID, "descendantsUIDs", uids, "error", err)
return err
}
return nil
}
func (s *Service) canMoveViaApiServer(ctx context.Context, cmd *folder.MoveFolderCommand) (bool, error) {
// Check that the user is allowed to move the folder to the destination folder
var evaluator accesscontrol.Evaluator
parentUID := cmd.NewParentUID
if parentUID != "" {
legacyEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.NewParentUID))
newEvaluator := accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.NewParentUID))
evaluator = accesscontrol.EvalAny(legacyEvaluator, newEvaluator)
} else {
// Evaluate folder creation permission when moving folder to the root level
evaluator = accesscontrol.EvalPermission(dashboards.ActionFoldersCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID))
parentUID = folder.GeneralFolderUID
}
if hasAccess, err := s.accessControl.Evaluate(ctx, cmd.SignedInUser, evaluator); err != nil {
return false, err
} else if !hasAccess {
return false, dashboards.ErrMoveAccessDenied.Errorf("user does not have permissions to move a folder to folder with UID %s", parentUID)
}
// Check that the user would not be elevating their permissions by moving a folder to the destination folder
// This is needed for plugins, as different folders can have different plugin configs
// We do this by checking that there are no permissions that user has on the destination parent folder but not on the source folder
// We also need to look at the folder tree for the destination folder, as folder permissions are inherited
newFolderAndParentUIDs, err := s.getFolderAndParentUIDScopesViaApiServer(ctx, parentUID, cmd.OrgID)
if err != nil {
return false, err
}
permissions := cmd.SignedInUser.GetPermissions()
var evaluators []accesscontrol.Evaluator
currentFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(cmd.UID)
for action, scopes := range permissions {
// Skip unexpanded action sets - they have no impact if action sets are not enabled
if !s.features.IsEnabled(ctx, featuremgmt.FlagAccessActionSets) {
if action == "folders:view" || action == "folders:edit" || action == "folders:admin" {
continue
}
}
for _, scope := range newFolderAndParentUIDs {
if slices.Contains(scopes, scope) {
evaluators = append(evaluators, accesscontrol.EvalPermission(action, currentFolderScope))
break
}
}
}
if hasAccess, err := s.accessControl.Evaluate(ctx, cmd.SignedInUser, accesscontrol.EvalAll(evaluators...)); err != nil {
return false, err
} else if !hasAccess {
return false, dashboards.ErrFolderAccessEscalation.Errorf("user cannot move a folder to another folder where they have higher permissions")
}
return true, nil
}
func (s *Service) getFolderAndParentUIDScopesViaApiServer(ctx context.Context, folderUID string, orgID int64) ([]string, error) {
folderAndParentUIDScopes := []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)}
if folderUID == folder.GeneralFolderUID {
return folderAndParentUIDScopes, nil
}
folderParents, err := s.unifiedStore.GetParents(ctx, folder.GetParentsQuery{UID: folderUID, OrgID: orgID})
if err != nil {
return nil, err
}
for _, newParent := range folderParents {
scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(newParent.UID)
folderAndParentUIDScopes = append(folderAndParentUIDScopes, scope)
}
return folderAndParentUIDScopes, nil
}
func (s *Service) getDescendantCountsFromApiServer(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
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")
}
if s.features.IsEnabledGlobally(featuremgmt.FlagK8SFolderCounts) {
return s.unifiedStore.(*FolderUnifiedStoreImpl).CountFolderContent(ctx, q.OrgID, *q.UID)
}
folders := []string{*q.UID}
countsMap := make(folder.DescendantCounts, len(s.registry)+1)
descendantFolders, err := s.unifiedStore.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
}
// -----------------------------------------------------------------------------------------
// Folder k8s functions
// -----------------------------------------------------------------------------------------
func (fk8s *foldk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
cfg := &rest.Config{
Host: fk8s.cfg.AppURL,
APIPath: "/apis",
TLSClientConfig: rest.TLSClientConfig{
Insecure: true, // Skip TLS verification
},
Username: fk8s.cfg.AdminUser,
Password: fk8s.cfg.AdminPassword,
}
dyn, err := dynamic.NewForConfig(cfg)
if err != nil {
return nil, false
}
return dyn.Resource(fk8s.gvr).Namespace(fk8s.getNamespace(orgID)), true
}
func (fk8s *foldk8sHandler) getNamespace(orgID int64) string {
return fk8s.namespacer(orgID)
}

View File

@ -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)
})
})
})
}

View File

@ -0,0 +1,520 @@
package folderimpl
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/apimachinery/identity"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8sUser "k8s.io/apiserver/pkg/authentication/user"
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type FolderUnifiedStoreImpl struct {
log log.Logger
k8sclient folderK8sHandler
}
// sqlStore implements the store interface.
var _ folder.Store = (*FolderUnifiedStoreImpl)(nil)
func ProvideUnifiedStore(cfg *setting.Cfg) *FolderUnifiedStoreImpl {
k8sHandler := &foldk8sHandler{
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
namespacer: request.GetNamespaceMapper(cfg),
cfg: cfg,
}
return &FolderUnifiedStoreImpl{
k8sclient: k8sHandler,
log: log.New("folder-store"),
}
}
func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateFolderCommand) (*folder.Folder, error) {
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, cmd.OrgID)
if !ok {
return nil, nil
}
obj, err := internalfolders.LegacyCreateCommandToUnstructured(&cmd)
if err != nil {
return nil, err
}
out, err := client.Create(newCtx, obj, v1.CreateOptions{})
if err != nil {
return nil, err
}
folder, _ := internalfolders.UnstructuredToLegacyFolder(*out, cmd.SignedInUser.GetOrgID())
if err != nil {
return nil, err
}
return folder, err
}
func (ss *FolderUnifiedStoreImpl) Delete(ctx context.Context, UIDs []string, orgID int64) error {
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, orgID)
if !ok {
return nil
}
for _, uid := range UIDs {
err = client.Delete(newCtx, uid, v1.DeleteOptions{})
if err != nil {
return err
}
}
return nil
}
func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateFolderCommand) (*folder.Folder, error) {
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, cmd.OrgID)
if !ok {
return nil, nil
}
obj, err := client.Get(ctx, cmd.UID, v1.GetOptions{})
if err != nil {
return nil, err
}
updated, err := internalfolders.LegacyUpdateCommandToUnstructured(obj, &cmd)
if err != nil {
return nil, err
}
out, err := client.Update(ctx, updated, v1.UpdateOptions{})
if err != nil {
return nil, err
}
folder, _ := internalfolders.UnstructuredToLegacyFolder(*out, cmd.SignedInUser.GetOrgID())
if err != nil {
return nil, err
}
return folder, err
}
// If WithFullpath is true it computes also the full path of a folder.
// The full path is a string that contains the titles of all parent folders separated by a slash.
// For example, if the folder structure is:
//
// A
// └── B
// └── C
//
// The full path of C is "A/B/C".
// The full path of B is "A/B".
// The full path of A is "A".
// If a folder contains a slash in its title, it is escaped with a backslash.
// For example, if the folder structure is:
//
// A
// └── B/C
//
// The full path of C is "A/B\/C".
func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQuery) (*folder.Folder, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, q.OrgID)
if !ok {
return nil, nil
}
out, err := client.Get(newCtx, *q.UID, v1.GetOptions{})
if err != nil {
return nil, err
}
dashFolder, _ := internalfolders.UnstructuredToLegacyFolder(*out, q.SignedInUser.GetOrgID())
return dashFolder, nil
}
func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, q.OrgID)
if !ok {
return nil, nil
}
hits := []*folder.Folder{}
parentUid := q.UID
for parentUid != "" {
out, err := client.Get(newCtx, parentUid, v1.GetOptions{})
if err != nil {
return nil, err
}
folder, _ := internalfolders.UnstructuredToLegacyFolder(*out, q.OrgID)
if err != nil {
return nil, err
}
parentUid = folder.ParentUID
hits = append(hits, folder)
}
if len(hits) > 0 {
return util.Reverse(hits[1:]), nil
}
return hits, nil
}
func (ss *FolderUnifiedStoreImpl) GetChildren(ctx context.Context, q folder.GetChildrenQuery) ([]*folder.Folder, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, q.OrgID)
if !ok {
return nil, nil
}
out, err := client.List(newCtx, v1.ListOptions{})
if err != nil {
return nil, err
}
hits := make([]*folder.Folder, 0)
for _, item := range out.Items {
// convert item to legacy folder format
f, _ := internalfolders.UnstructuredToLegacyFolder(item, q.OrgID)
if f == nil {
return nil, fmt.Errorf("unable covert unstructured item to legacy folder")
}
// it we are at root level, skip subfolder
if q.UID == "" && f.ParentUID != "" {
continue // query filter
}
// if we are at a nested folder, then skip folders that don't belong to parentUid
if q.UID != "" && !strings.EqualFold(f.ParentUID, q.UID) {
continue
}
hits = append(hits, f)
}
return hits, nil
}
// TODO use a single query to get the height of a folder
func (ss *FolderUnifiedStoreImpl) GetHeight(ctx context.Context, foldrUID string, orgID int64, parentUID *string) (int, error) {
height := -1
queue := []string{foldrUID}
for len(queue) > 0 && height <= folder.MaxNestedFolderDepth {
length := len(queue)
height++
for i := 0; i < length; i++ {
ele := queue[0]
queue = queue[1:]
if parentUID != nil && *parentUID == ele {
return 0, folder.ErrCircularReference
}
folders, err := ss.GetChildren(ctx, folder.GetChildrenQuery{UID: ele, OrgID: orgID})
if err != nil {
return 0, err
}
for _, f := range folders {
queue = append(queue, f.UID)
}
}
}
if height > folder.MaxNestedFolderDepth {
ss.log.Warn("folder height exceeds the maximum allowed depth, You might have a circular reference", "uid", foldrUID, "orgId", orgID, "maxDepth", folder.MaxNestedFolderDepth)
}
return height, nil
}
// GetFolders returns org folders by their UIDs.
// If UIDs is empty, it returns all folders in the org.
// If WithFullpath is true it computes also the full path of a folder.
// The full path is a string that contains the titles of all parent folders separated by a slash.
// For example, if the folder structure is:
//
// A
// └── B
// └── C
//
// The full path of C is "A/B/C".
// The full path of B is "A/B".
// The full path of A is "A".
// If a folder contains a slash in its title, it is escaped with a backslash.
// For example, if the folder structure is:
//
// A
// └── B/C
//
// The full path of C is "A/B\/C".
//
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
// For example, if the folder structure is:
//
// A (uid: "uid1")
// └── B (uid: "uid2")
// └── C (uid: "uid3")
//
// The full path UIDs of C is "uid1/uid2/uid3".
// 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) {
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, q.OrgID)
if !ok {
return nil, nil
}
out, err := client.List(newCtx, v1.ListOptions{})
if err != nil {
return nil, err
}
m := map[string]*folder.Folder{}
for _, item := range out.Items {
// convert item to legacy folder format
f, _ := internalfolders.UnstructuredToLegacyFolder(item, q.SignedInUser.GetOrgID())
if f == nil {
return nil, fmt.Errorf("unable covert unstructured item to legacy folder")
}
m[f.UID] = f
}
hits := []*folder.Folder{}
if len(q.UIDs) > 0 {
//return only the specified q.UIDs
for _, uid := range q.UIDs {
f, ok := m[uid]
if ok {
hits = append(hits, f)
}
}
return hits, nil
}
/*
if len(q.AncestorUIDs) > 0 {
// TODO
//return all nodes under those ancestors, requires building a tree
}
*/
//return everything
for _, f := range m {
hits = append(hits, f)
}
return hits, nil
}
func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int64, ancestor_uid string) ([]*folder.Folder, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, orgID)
if !ok {
return nil, nil
}
out, err := client.List(newCtx, v1.ListOptions{})
if err != nil {
return nil, err
}
nodes := map[string]*folder.Folder{}
for _, item := range out.Items {
// convert item to legacy folder format
f, _ := internalfolders.UnstructuredToLegacyFolder(item, orgID)
if f == nil {
return nil, fmt.Errorf("unable covert unstructured item to legacy folder")
}
nodes[f.UID] = f
}
tree := map[string]map[string]*folder.Folder{}
for uid, f := range nodes {
parentUID := f.ParentUID
if parentUID == "" {
parentUID = "general"
}
if tree[parentUID] == nil {
tree[parentUID] = map[string]*folder.Folder{}
}
tree[parentUID][uid] = f
}
descendantsMap := map[string]*folder.Folder{}
getDescendants(nodes, tree, ancestor_uid, descendantsMap)
descendants := []*folder.Folder{}
for _, f := range descendantsMap {
descendants = append(descendants, f)
}
return descendants, nil
}
func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string]*folder.Folder, ancestor_uid string, descendantsMap map[string]*folder.Folder) {
for uid, _ := range tree[ancestor_uid] {
descendantsMap[uid] = nodes[uid]
getDescendants(nodes, tree, uid, descendantsMap)
}
}
func (ss *FolderUnifiedStoreImpl) CountFolderContent(ctx context.Context, orgID int64, ancestor_uid string) (folder.DescendantCounts, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := ss.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := ss.k8sclient.getClient(newCtx, orgID)
if !ok {
return nil, nil
}
counts, err := client.Get(newCtx, ancestor_uid, v1.GetOptions{}, "counts")
if err != nil {
return nil, err
}
res, err := toFolderLegacyCounts(counts)
return *res, err
}
func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) {
ds, err := v0alpha1.UnstructuredToDescendantCounts(u)
if err != nil {
return nil, err
}
var out = make(folder.DescendantCounts)
for _, v := range ds.Counts {
// if stats come from unified storage, we will use them
if v.Group != "sql-fallback" {
out[v.Resource] = v.Count
continue
}
// if stats are from single tenant DB and they are not in unified storage, we will use them
if _, ok := out[v.Resource]; !ok {
out[v.Resource] = v.Count
}
}
return &out, nil
}
func (ss *FolderUnifiedStoreImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) {
requester, requesterErr := identity.GetRequester(ctx)
if requesterErr != nil {
return nil, nil, requesterErr
}
user, exists := k8sRequest.UserFrom(ctx)
if !exists {
// add in k8s user if not there yet
var ok bool
user, ok = requester.(k8sUser.Info)
if !ok {
return nil, nil, fmt.Errorf("could not convert user to k8s user")
}
}
newCtx := k8sRequest.WithUser(context.Background(), user)
newCtx = log.WithContextualAttributes(newCtx, log.FromContext(ctx))
// TODO: after GLSA token workflow is removed, make this return early
// and move the else below to be unconditional
if requesterErr == nil {
newCtxWithRequester := identity.WithRequester(newCtx, requester)
newCtx = newCtxWithRequester
}
// inherit the deadline from the original context, if it exists
deadline, ok := ctx.Deadline()
if ok {
var newCancel context.CancelFunc
newCtx, newCancel = context.WithTimeout(newCtx, time.Until(deadline))
return newCtx, newCancel, nil
}
return newCtx, nil, nil
}

View File

@ -22,27 +22,51 @@ var _ folder.Service = (*FakeService)(nil)
func (s *FakeService) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
return s.ExpectedFolders, s.ExpectedError
}
func (s *FakeService) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
return s.ExpectedFolders, 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
}
func (s *FakeService) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
return s.ExpectedError
}
func (s *FakeService) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
return s.ExpectedError
}
func (s *FakeService) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) MoveLegacy(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) RegisterService(service folder.RegistryService) error {
return s.ExpectedError
@ -51,7 +75,13 @@ 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) {
return s.ExpectedFolders, s.ExpectedError
}
func (s *FakeService) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
return s.ExpectedFolders, s.ExpectedError
}

View File

@ -5,12 +5,10 @@ import (
)
type Service interface {
// GetChildren returns an array containing all child folders.
GetChildren(ctx context.Context, q *GetChildrenQuery) ([]*Folder, 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)
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,21 +18,39 @@ 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)
RegisterService(service RegistryService) 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.
// The full path is a string that contains the titles of all parent folders separated by a slash.
// 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)
// GetChildren returns an array containing all child folders.
GetChildren(ctx context.Context, q *GetChildrenQuery) ([]*Folder, error)
GetChildrenLegacy(ctx context.Context, q *GetChildrenQuery) ([]*Folder, 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)
}
// FolderStore is a folder store.

View File

@ -336,7 +336,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
store := folderimpl.ProvideStore(sc.sqlStore)
s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore,
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
t.Logf("Creating folder with title and UID %q", title)
ctx := identity.WithRequester(context.Background(), &sc.user)
folder, err := s.Create(ctx, &folder.CreateFolderCommand{
@ -473,7 +473,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
guardian.InitAccessControlGuardian(cfg, ac, dashService)
fStore := folderimpl.ProvideStore(sqlStore)
folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore,
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
service := LibraryElementService{
Cfg: cfg,
features: featuremgmt.WithFeatures(),

View File

@ -755,7 +755,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
fStore := folderimpl.ProvideStore(sc.sqlStore)
s := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore,
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
t.Logf("Creating folder with title and UID %q", title)
ctx := identity.WithRequester(context.Background(), sc.user)
@ -840,7 +840,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
fStore := folderimpl.ProvideStore(sqlStore)
folderService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore,
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac)
service := LibraryPanelService{

View File

@ -1920,7 +1920,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderService := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore,
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
store := store.DBstore{
Logger: log,
SQLStore: sqlStore,

View File

@ -1604,7 +1604,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
fStore := folderimpl.ProvideStore(sqlStore)
folderService := folderimpl.ProvideService(fStore, ac, inProcBus, dashboardStore, folderStore, sqlStore,
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
ruleService := createAlertRuleService(t, folderService)
var orgID int64 = 1

View File

@ -30,7 +30,7 @@ func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStor
tb.Helper()
fStore := folderimpl.ProvideStore(db)
return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, db,
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
}
func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) {

View File

@ -1395,7 +1395,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
fStore := folderimpl.ProvideStore(testDB)
folderPermissions := acmock.NewMockedPermissionsService()
folderStore := folderimpl.ProvideDashboardFolderStore(testDB)
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, testDB, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, testDB, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, &actest.FakePermissionsService{}, ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
require.NoError(t, err)

View File

@ -494,7 +494,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
folderSvc := folderimpl.ProvideService(fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
dashStore, folderStore, sqlStore, featuremgmt.WithFeatures(),
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
_, err = dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotaService, nil)

View File

@ -823,7 +823,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
fStore := folderimpl.ProvideStore(db)
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
// create parent folder
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{

View File

@ -80,7 +80,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
fStore := folderimpl.ProvideStore(store)
folderSvc := folderimpl.ProvideService(fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store),
store, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
store, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
origNewGuardian := guardian.New
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})

View File

@ -100,6 +100,8 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
@ -559,6 +561,10 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGA
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
github.com/grafana/grafana-plugin-sdk-go v0.261.0 h1:pGGpPbKRWZcLxwNATEiVDhILbYGYwlWOEXFLhmUkBMo=
github.com/grafana/grafana-plugin-sdk-go v0.261.0/go.mod h1:QsLK0kAbmDXuX/QncFBTETPHCzw5g9hZnzqOPkoB3Yo=
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI=
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2/go.mod h1:gBLBniiSUQvyt4LRrpIeysj8Many0DV+hdUKifRE0Ec=
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435/go.mod h1:8cz+z0i57IjN6MYmu/zZQdCg9CQcsnEHbaJBBEf3KQo=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
@ -775,6 +781,8 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR
github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -792,6 +800,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
@ -1546,6 +1556,10 @@ k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU=
k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kms v0.32.0 h1:jwOfunHIrcdYl5FRcA+uUKKtg6qiqoPCwmS2T3XTYL4=
k8s.io/kms v0.32.0/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM=
k8s.io/kube-aggregator v0.32.0 h1:5ZyMW3QwAbmkasQrROcpa5we3et938DQuyUYHeXSPao=
k8s.io/kube-aggregator v0.32.0/go.mod h1:6OKivf6Ypx44qu2v1ZUMrxH8kRp/8LKFKeJU72J18lU=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=

View File

@ -51,7 +51,7 @@ func TestDirectSQLStats(t *testing.T) {
fStore := folderimpl.ProvideStore(db)
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
folderimpl.ProvideDashboardFolderStore(db), db, featuremgmt.WithFeatures(),
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
// create parent folder

View File

@ -925,7 +925,6 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
}
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
folderWithoutUID := "{ \"title\": \"Folder without UID\"}"
folderWithTitleEmpty := "{ \"title\": \"\"}"
folderWithInvalidUid := "{ \"uid\": \"::::::::::::\", \"title\": \"Another folder\"}"
folderWithUIDTooLong := "{ \"uid\": \"asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm\", \"title\": \"Third folder\"}"
@ -964,17 +963,6 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
expectedMessage: dashboards.ErrFolderAccessDenied.Error(),
permissions: []resourcepermissions.SetResourcePermissionCommand{},
},
{
// #TODO This test case doesn't set up the conditions it describes. We should have created a folder with the same UID before
// creating a second one and failing to do so successfully.
description: "folder creation fails given folder service error %s",
input: folderWithoutUID,
expectedCode: http.StatusConflict,
// expectedMessage: dashboards.ErrFolderWithSameUIDExists.Error(),
expectedFolderSvcError: dashboards.ErrFolderWithSameUIDExists,
createSecondRecord: true,
permissions: folderCreatePermission,
},
{
description: "folder creation fails given folder service error %s",
input: folderWithTitleEmpty,