mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
009d7f42b3
commit
766d645d82
@ -173,6 +173,7 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally |
|
| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally |
|
||||||
| `kubernetesRestore` | Allow restoring objects in k8s |
|
| `kubernetesRestore` | Allow restoring objects in k8s |
|
||||||
| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to 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 |
|
| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs |
|
||||||
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
|
| `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 |
|
| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query |
|
||||||
|
@ -113,6 +113,7 @@ export interface FeatureToggles {
|
|||||||
kubernetesCliDashboards?: boolean;
|
kubernetesCliDashboards?: boolean;
|
||||||
kubernetesRestore?: boolean;
|
kubernetesRestore?: boolean;
|
||||||
kubernetesFolders?: boolean;
|
kubernetesFolders?: boolean;
|
||||||
|
kubernetesFoldersServiceV2?: boolean;
|
||||||
grafanaAPIServerTestingWithExperimentalAPIs?: boolean;
|
grafanaAPIServerTestingWithExperimentalAPIs?: boolean;
|
||||||
datasourceQueryTypes?: boolean;
|
datasourceQueryTypes?: boolean;
|
||||||
queryService?: boolean;
|
queryService?: boolean;
|
||||||
|
@ -834,7 +834,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
|||||||
quotaService := quotatest.New(false, nil)
|
quotaService := quotatest.New(false, nil)
|
||||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||||
dashboardStore, folderStore, db, features,
|
dashboardStore, folderStore, db, features,
|
||||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||||
if dashboardService == nil {
|
if dashboardService == nil {
|
||||||
dashboardService, err = service.ProvideDashboardServiceImpl(
|
dashboardService, err = service.ProvideDashboardServiceImpl(
|
||||||
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
||||||
|
@ -57,7 +57,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
|||||||
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
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
|
// Use k8s client to implement legacy API
|
||||||
handler := newFolderK8sHandler(hs)
|
handler := newFolderK8sHandler(hs)
|
||||||
folderRoute.Post("/", handler.createFolder)
|
folderRoute.Post("/", handler.createFolder)
|
||||||
|
@ -461,7 +461,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
|||||||
actionSets := resourcepermissions.NewActionSetService(features)
|
actionSets := resourcepermissions.NewActionSetService(features)
|
||||||
fStore := folderimpl.ProvideStore(sc.db)
|
fStore := folderimpl.ProvideStore(sc.db)
|
||||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
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(
|
acSvc := acimpl.ProvideOSSService(
|
||||||
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
||||||
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn,
|
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn,
|
||||||
|
@ -52,6 +52,11 @@ func LegacyUpdateCommandToUnstructured(obj *unstructured.Unstructured, cmd *fold
|
|||||||
if cmd.NewDescription != nil {
|
if cmd.NewDescription != nil {
|
||||||
spec["description"] = cmd.NewDescription
|
spec["description"] = cmd.NewDescription
|
||||||
}
|
}
|
||||||
|
if cmd.NewParentUID != nil {
|
||||||
|
if err := setParentUID(obj, *cmd.NewParentUID); err != nil {
|
||||||
|
return &unstructured.Unstructured{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return obj, nil
|
return obj, nil
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List must return all folders
|
// List must return all folders
|
||||||
hits, err := s.service.GetFolders(ctx, folder.GetFoldersQuery{
|
hits, err := s.service.GetFoldersLegacy(ctx, folder.GetFoldersQuery{
|
||||||
SignedInUser: user,
|
SignedInUser: user,
|
||||||
OrgID: orgId,
|
OrgID: orgId,
|
||||||
// TODO: enable pagination
|
// TODO: enable pagination
|
||||||
@ -133,7 +133,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dto, err := s.service.Get(ctx, &folder.GetFolderQuery{
|
dto, err := s.service.GetLegacy(ctx, &folder.GetFolderQuery{
|
||||||
SignedInUser: user,
|
SignedInUser: user,
|
||||||
UID: &name,
|
UID: &name,
|
||||||
OrgID: info.OrgID,
|
OrgID: info.OrgID,
|
||||||
@ -186,7 +186,7 @@ func (s *legacyStorage) Create(ctx context.Context,
|
|||||||
|
|
||||||
parent := accessor.GetFolder()
|
parent := accessor.GetFolder()
|
||||||
|
|
||||||
out, err := s.service.Create(ctx, &folder.CreateFolderCommand{
|
out, err := s.service.CreateLegacy(ctx, &folder.CreateFolderCommand{
|
||||||
SignedInUser: user,
|
SignedInUser: user,
|
||||||
UID: p.Name,
|
UID: p.Name,
|
||||||
Title: p.Spec.Title,
|
Title: p.Spec.Title,
|
||||||
@ -285,7 +285,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
|||||||
oldParent := mOld.GetFolder()
|
oldParent := mOld.GetFolder()
|
||||||
newParent := mNew.GetFolder()
|
newParent := mNew.GetFolder()
|
||||||
if oldParent != newParent {
|
if oldParent != newParent {
|
||||||
_, err = s.service.Move(ctx, &folder.MoveFolderCommand{
|
_, err = s.service.MoveLegacy(ctx, &folder.MoveFolderCommand{
|
||||||
SignedInUser: user,
|
SignedInUser: user,
|
||||||
UID: name,
|
UID: name,
|
||||||
OrgID: info.OrgID,
|
OrgID: info.OrgID,
|
||||||
@ -312,7 +312,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
|||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
_, err = s.service.Update(ctx, cmd)
|
_, err = s.service.UpdateLegacy(ctx, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
@ -340,7 +340,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
|
|||||||
if !ok {
|
if !ok {
|
||||||
return v, false, fmt.Errorf("expected a folder response from Get")
|
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,
|
UID: name,
|
||||||
OrgID: info.OrgID,
|
OrgID: info.OrgID,
|
||||||
SignedInUser: user,
|
SignedInUser: user,
|
||||||
|
@ -66,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
|||||||
) *FolderAPIBuilder {
|
) *FolderAPIBuilder {
|
||||||
if !featuremgmt.AnyEnabled(features,
|
if !featuremgmt.AnyEnabled(features,
|
||||||
featuremgmt.FlagKubernetesFolders,
|
featuremgmt.FlagKubernetesFolders,
|
||||||
|
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||||
featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs,
|
featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs,
|
||||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||||
featuremgmt.FlagProvisioning) {
|
featuremgmt.FlagProvisioning) {
|
||||||
|
@ -49,7 +49,7 @@ func ProvideFolderPermissions(
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||||
fService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
fService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||||
dashboardStore, folderStore, sqlStore, features,
|
dashboardStore, folderStore, sqlStore, features,
|
||||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||||
|
|
||||||
acSvc := acimpl.ProvideOSSService(
|
acSvc := acimpl.ProvideOSSService(
|
||||||
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),
|
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),
|
||||||
|
@ -50,7 +50,7 @@ func TestIntegrationAuthorize(t *testing.T) {
|
|||||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||||
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||||
dashStore, folderStore, sql, featuremgmt.WithFeatures(),
|
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(),
|
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)
|
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -62,7 +62,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
|||||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||||
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||||
dashStore, folderStore, sql, featuremgmt.WithFeatures(),
|
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(),
|
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)
|
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -244,7 +244,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
|
|||||||
fStore := folderimpl.ProvideStore(sql)
|
fStore := folderimpl.ProvideStore(sql)
|
||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
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(),
|
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)
|
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -300,7 +300,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
folderStore := folderimpl.ProvideStore(sqlStore)
|
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 := ""
|
parentUID := ""
|
||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
|
@ -891,7 +891,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||||
fStore := folderimpl.ProvideStore(sqlStore)
|
fStore := folderimpl.ProvideStore(sqlStore)
|
||||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
|
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{
|
user := &user.SignedInUser{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
@ -1010,7 +1010,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
|
|||||||
fStore := folderimpl.ProvideStore(sqlStore)
|
fStore := folderimpl.ProvideStore(sqlStore)
|
||||||
|
|
||||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
|
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{
|
user := &user.SignedInUser{
|
||||||
OrgID: 1,
|
OrgID: 1,
|
||||||
|
@ -372,13 +372,15 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
|
|||||||
|
|
||||||
// Validate folder
|
// Validate folder
|
||||||
if dash.FolderUID != "" {
|
if dash.FolderUID != "" {
|
||||||
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
|
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||||
if err != nil {
|
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
|
||||||
return nil, err
|
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
|
} else if dash.FolderID != 0 { // nolint:staticcheck
|
||||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
|
@ -101,6 +101,7 @@ func TestIntegrationDashboardServiceZanzana(t *testing.T) {
|
|||||||
db,
|
db,
|
||||||
featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
||||||
supportbundlestest.NewFakeBundleService(),
|
supportbundlestest.NewFakeBundleService(),
|
||||||
|
cfg,
|
||||||
nil,
|
nil,
|
||||||
tracing.InitializeTracerForTest(),
|
tracing.InitializeTracerForTest(),
|
||||||
)
|
)
|
||||||
|
@ -714,6 +714,12 @@ var (
|
|||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStageExperimental,
|
||||||
Owner: grafanaSearchAndStorageSquad,
|
Owner: grafanaSearchAndStorageSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "kubernetesFoldersServiceV2",
|
||||||
|
Description: "Use the Folders Service V2, and route Folder Service requests to k8s",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
Owner: grafanaSearchAndStorageSquad,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "grafanaAPIServerTestingWithExperimentalAPIs",
|
Name: "grafanaAPIServerTestingWithExperimentalAPIs",
|
||||||
Description: "Facilitate integration testing of experimental APIs",
|
Description: "Facilitate integration testing of experimental APIs",
|
||||||
|
@ -94,6 +94,7 @@ kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,fals
|
|||||||
kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||||
kubernetesRestore,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
|
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
|
grafanaAPIServerTestingWithExperimentalAPIs,experimental,@grafana/search-and-storage,false,false,false
|
||||||
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||||
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||||
|
|
@ -387,6 +387,10 @@ const (
|
|||||||
// Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s
|
// Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s
|
||||||
FlagKubernetesFolders = "kubernetesFolders"
|
FlagKubernetesFolders = "kubernetesFolders"
|
||||||
|
|
||||||
|
// FlagKubernetesFoldersServiceV2
|
||||||
|
// Use the Folders Service V2, and route Folder Service requests to k8s
|
||||||
|
FlagKubernetesFoldersServiceV2 = "kubernetesFoldersServiceV2"
|
||||||
|
|
||||||
// FlagGrafanaAPIServerTestingWithExperimentalAPIs
|
// FlagGrafanaAPIServerTestingWithExperimentalAPIs
|
||||||
// Facilitate integration testing of experimental APIs
|
// Facilitate integration testing of experimental APIs
|
||||||
FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs"
|
FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs"
|
||||||
|
@ -2081,6 +2081,18 @@
|
|||||||
"codeowner": "@grafana/search-and-storage"
|
"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": {
|
"metadata": {
|
||||||
"name": "kubernetesPlaylists",
|
"name": "kubernetesPlaylists",
|
||||||
|
@ -18,12 +18,14 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"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/bus"
|
||||||
"github.com/grafana/grafana/pkg/events"
|
"github.com/grafana/grafana/pkg/events"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"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/store/entity"
|
||||||
"github.com/grafana/grafana/pkg/services/supportbundles"
|
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,12 +44,14 @@ const FULLPATH_SEPARATOR = "/"
|
|||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
store folder.Store
|
store folder.Store
|
||||||
|
unifiedStore folder.Store
|
||||||
db db.DB
|
db db.DB
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
dashboardStore dashboards.Store
|
dashboardStore dashboards.Store
|
||||||
dashboardFolderStore folder.FolderStore
|
dashboardFolderStore folder.FolderStore
|
||||||
features featuremgmt.FeatureToggles
|
features featuremgmt.FeatureToggles
|
||||||
accessControl accesscontrol.AccessControl
|
accessControl accesscontrol.AccessControl
|
||||||
|
k8sclient folderK8sHandler
|
||||||
// bus is currently used to publish event in case of folder full path change.
|
// 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.
|
// For example when a folder is moved to another folder or when a folder is renamed.
|
||||||
bus bus.Bus
|
bus bus.Bus
|
||||||
@ -66,14 +71,24 @@ func ProvideService(
|
|||||||
db db.DB, // DB for the (new) nested folder store
|
db db.DB, // DB for the (new) nested folder store
|
||||||
features featuremgmt.FeatureToggles,
|
features featuremgmt.FeatureToggles,
|
||||||
supportBundles supportbundles.Service,
|
supportBundles supportbundles.Service,
|
||||||
|
cfg *setting.Cfg,
|
||||||
r prometheus.Registerer,
|
r prometheus.Registerer,
|
||||||
tracer tracing.Tracer,
|
tracer tracing.Tracer,
|
||||||
) *Service {
|
) *Service {
|
||||||
|
k8sHandler := &foldk8sHandler{
|
||||||
|
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
||||||
|
namespacer: request.GetNamespaceMapper(cfg),
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
unifiedStore := ProvideUnifiedStore(cfg)
|
||||||
|
|
||||||
srv := &Service{
|
srv := &Service{
|
||||||
log: slog.Default().With("logger", "folder-service"),
|
log: slog.Default().With("logger", "folder-service"),
|
||||||
dashboardStore: dashboardStore,
|
dashboardStore: dashboardStore,
|
||||||
dashboardFolderStore: folderStore,
|
dashboardFolderStore: folderStore,
|
||||||
store: store,
|
store: store,
|
||||||
|
unifiedStore: unifiedStore,
|
||||||
features: features,
|
features: features,
|
||||||
accessControl: ac,
|
accessControl: ac,
|
||||||
bus: bus,
|
bus: bus,
|
||||||
@ -81,6 +96,7 @@ func ProvideService(
|
|||||||
registry: make(map[string]folder.RegistryService),
|
registry: make(map[string]folder.RegistryService),
|
||||||
metrics: newFoldersMetrics(r),
|
metrics: newFoldersMetrics(r),
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
|
k8sclient: k8sHandler,
|
||||||
}
|
}
|
||||||
srv.DBMigration(db)
|
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) {
|
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 {
|
if q.SignedInUser == nil {
|
||||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
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) {
|
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 {
|
if q.SignedInUser == nil {
|
||||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
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) {
|
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
|
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
|
// #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
|
// it gets parsed using UserIdentifier(). Also is there some kind of validation taking place as
|
||||||
// part of the parsing?
|
// 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
|
// 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
|
// are not yet present for the user--this requires a call to ClearUserPermissionCache
|
||||||
parents, err := s.GetParents(ctx, folder.GetParentsQuery{
|
var parents []*folder.Folder
|
||||||
UID: f.UID,
|
var err error
|
||||||
OrgID: f.OrgID,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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) {
|
defer func(t time.Time) {
|
||||||
parent := q.UID
|
parent := q.UID
|
||||||
if q.UID != folder.SharedWithMeFolderUID {
|
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 {
|
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 == "" {
|
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
|
// 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()
|
start := time.Now()
|
||||||
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q)
|
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q, forceLegacy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
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)
|
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 {
|
if err != nil {
|
||||||
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
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)
|
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
|
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()
|
permissions := q.SignedInUser.GetPermissions()
|
||||||
var folderPermissions []string
|
var folderPermissions []string
|
||||||
if q.Permission == dashboardaccess.PERMISSION_EDIT {
|
if q.Permission == dashboardaccess.PERMISSION_EDIT {
|
||||||
@ -507,13 +558,25 @@ func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetC
|
|||||||
return nonRootFolders, nil
|
return nonRootFolders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dashFolders, err := s.GetFolders(ctx, folder.GetFoldersQuery{
|
var dashFolders []*folder.Folder
|
||||||
UIDs: folderUids,
|
var err error
|
||||||
OrgID: q.OrgID,
|
if forceLegacy {
|
||||||
SignedInUser: q.SignedInUser,
|
dashFolders, err = s.GetFoldersLegacy(ctx, folder.GetFoldersQuery{
|
||||||
OrderByTitle: true,
|
UIDs: folderUids,
|
||||||
WithFullpathUIDs: true,
|
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 {
|
if err != nil {
|
||||||
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
|
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) {
|
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 {
|
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) || q.UID == accesscontrol.GeneralFolderUID {
|
||||||
return nil, nil
|
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) {
|
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() {
|
if cmd.SignedInUser == nil || cmd.SignedInUser.IsNil() {
|
||||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
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) {
|
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
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
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")
|
ctx, span := s.tracer.Start(ctx, "folder.Update")
|
||||||
defer span.End()
|
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 {
|
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 {
|
if cmd.SignedInUser == nil {
|
||||||
return folder.ErrBadRequest.Errorf("missing signed in user")
|
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) {
|
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")
|
ctx, span := s.tracer.Start(ctx, "folder.Move")
|
||||||
defer span.End()
|
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) {
|
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 {
|
if q.SignedInUser == nil {
|
||||||
return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
|
return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
|
||||||
}
|
}
|
||||||
|
@ -62,10 +62,10 @@ func TestIntegrationProvideFolderService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Run("should register scope resolvers", func(t *testing.T) {
|
t.Run("should register scope resolvers", func(t *testing.T) {
|
||||||
ac := acmock.New()
|
ac := acmock.New()
|
||||||
db, _ := db.InitTestDBWithCfg(t)
|
db, cfg := db.InitTestDBWithCfg(t)
|
||||||
store := ProvideStore(db)
|
store := ProvideStore(db)
|
||||||
ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, 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)
|
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
|
||||||
})
|
})
|
||||||
|
702
pkg/services/folder/folderimpl/folder_unifiedstorage.go
Normal file
702
pkg/services/folder/folderimpl/folder_unifiedstorage.go
Normal 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)
|
||||||
|
}
|
414
pkg/services/folder/folderimpl/folder_unifiedstorage_test.go
Normal file
414
pkg/services/folder/folderimpl/folder_unifiedstorage_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
520
pkg/services/folder/folderimpl/unifiedstore.go
Normal file
520
pkg/services/folder/folderimpl/unifiedstore.go
Normal 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
|
||||||
|
}
|
@ -22,27 +22,51 @@ var _ folder.Service = (*FakeService)(nil)
|
|||||||
func (s *FakeService) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
func (s *FakeService) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||||
return s.ExpectedFolders, s.ExpectedError
|
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) {
|
func (s *FakeService) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
||||||
return s.ExpectedFolders, s.ExpectedError
|
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) {
|
func (s *FakeService) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||||
return s.ExpectedFolder, s.ExpectedError
|
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) {
|
func (s *FakeService) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||||
return s.ExpectedFolder, s.ExpectedError
|
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) {
|
func (s *FakeService) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
||||||
return s.ExpectedFolder, s.ExpectedError
|
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 {
|
func (s *FakeService) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
|
||||||
return s.ExpectedError
|
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) {
|
func (s *FakeService) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
|
||||||
return s.ExpectedFolder, s.ExpectedError
|
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 {
|
func (s *FakeService) RegisterService(service folder.RegistryService) error {
|
||||||
return s.ExpectedError
|
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) {
|
func (s *FakeService) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||||
return s.ExpectedDescendantCounts, s.ExpectedError
|
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) {
|
func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||||
return s.ExpectedFolders, s.ExpectedError
|
return s.ExpectedFolders, s.ExpectedError
|
||||||
}
|
}
|
||||||
|
func (s *FakeService) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||||
|
return s.ExpectedFolders, s.ExpectedError
|
||||||
|
}
|
||||||
|
@ -5,12 +5,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
// GetChildren returns an array containing all child folders.
|
RegisterService(service RegistryService) error
|
||||||
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)
|
|
||||||
Create(ctx context.Context, cmd *CreateFolderCommand) (*Folder, 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
|
// GetFolder takes a GetFolderCommand and returns a folder matching the
|
||||||
// request. One of UID, ID or Title must be included. If multiple values
|
// 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 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.
|
// If WithFullpath is true it computes also the full path of a folder.
|
||||||
Get(ctx context.Context, q *GetFolderQuery) (*Folder, error)
|
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
|
// Update is used to update a folder's UID, Title and Description. To change
|
||||||
// a folder's parent folder, use Move.
|
// a folder's parent folder, use Move.
|
||||||
Update(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
|
Update(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
|
||||||
|
UpdateLegacy(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
|
||||||
|
|
||||||
Delete(ctx context.Context, cmd *DeleteFolderCommand) 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 changes a folder's parent folder to the requested new parent.
|
||||||
Move(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
|
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.
|
// 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.
|
// 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.
|
// 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 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.
|
// 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)
|
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)
|
GetDescendantCounts(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
|
||||||
|
GetDescendantCountsLegacy(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FolderStore is a folder store.
|
// FolderStore is a folder store.
|
||||||
|
@ -336,7 +336,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
|
||||||
store := folderimpl.ProvideStore(sc.sqlStore)
|
store := folderimpl.ProvideStore(sc.sqlStore)
|
||||||
s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, 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)
|
t.Logf("Creating folder with title and UID %q", title)
|
||||||
ctx := identity.WithRequester(context.Background(), &sc.user)
|
ctx := identity.WithRequester(context.Background(), &sc.user)
|
||||||
folder, err := s.Create(ctx, &folder.CreateFolderCommand{
|
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)
|
guardian.InitAccessControlGuardian(cfg, ac, dashService)
|
||||||
fStore := folderimpl.ProvideStore(sqlStore)
|
fStore := folderimpl.ProvideStore(sqlStore)
|
||||||
folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, 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{
|
service := LibraryElementService{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
features: featuremgmt.WithFeatures(),
|
features: featuremgmt.WithFeatures(),
|
||||||
|
@ -755,7 +755,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
|
||||||
fStore := folderimpl.ProvideStore(sc.sqlStore)
|
fStore := folderimpl.ProvideStore(sc.sqlStore)
|
||||||
s := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, 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)
|
t.Logf("Creating folder with title and UID %q", title)
|
||||||
ctx := identity.WithRequester(context.Background(), sc.user)
|
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)
|
fStore := folderimpl.ProvideStore(sqlStore)
|
||||||
|
|
||||||
folderService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, 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)
|
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac)
|
||||||
service := LibraryPanelService{
|
service := LibraryPanelService{
|
||||||
|
@ -1920,7 +1920,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
|
|||||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||||
fStore := folderimpl.ProvideStore(sqlStore)
|
fStore := folderimpl.ProvideStore(sqlStore)
|
||||||
folderService := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, 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{
|
store := store.DBstore{
|
||||||
Logger: log,
|
Logger: log,
|
||||||
SQLStore: sqlStore,
|
SQLStore: sqlStore,
|
||||||
|
@ -1604,7 +1604,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
|
|||||||
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||||
fStore := folderimpl.ProvideStore(sqlStore)
|
fStore := folderimpl.ProvideStore(sqlStore)
|
||||||
folderService := folderimpl.ProvideService(fStore, ac, inProcBus, dashboardStore, folderStore, 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)
|
ruleService := createAlertRuleService(t, folderService)
|
||||||
var orgID int64 = 1
|
var orgID int64 = 1
|
||||||
|
@ -30,7 +30,7 @@ func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStor
|
|||||||
tb.Helper()
|
tb.Helper()
|
||||||
fStore := folderimpl.ProvideStore(db)
|
fStore := folderimpl.ProvideStore(db)
|
||||||
return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, 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) {
|
func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) {
|
||||||
|
@ -1395,7 +1395,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
|
|||||||
fStore := folderimpl.ProvideStore(testDB)
|
fStore := folderimpl.ProvideStore(testDB)
|
||||||
folderPermissions := acmock.NewMockedPermissionsService()
|
folderPermissions := acmock.NewMockedPermissionsService()
|
||||||
folderStore := folderimpl.ProvideDashboardFolderStore(testDB)
|
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)
|
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)
|
require.NoError(t, err)
|
||||||
|
@ -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())
|
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||||
folderSvc := folderimpl.ProvideService(fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
folderSvc := folderimpl.ProvideService(fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||||
dashStore, folderStore, sqlStore, featuremgmt.WithFeatures(),
|
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(),
|
_, err = dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(),
|
||||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotaService, nil)
|
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotaService, nil)
|
||||||
|
|
||||||
|
@ -823,7 +823,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
|
|||||||
|
|
||||||
fStore := folderimpl.ProvideStore(db)
|
fStore := folderimpl.ProvideStore(db)
|
||||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
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
|
// create parent folder
|
||||||
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||||
|
@ -80,7 +80,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
|
|||||||
|
|
||||||
fStore := folderimpl.ProvideStore(store)
|
fStore := folderimpl.ProvideStore(store)
|
||||||
folderSvc := folderimpl.ProvideService(fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(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
|
origNewGuardian := guardian.New
|
||||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||||
|
@ -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.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
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/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 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
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-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 h1:pGGpPbKRWZcLxwNATEiVDhILbYGYwlWOEXFLhmUkBMo=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.261.0/go.mod h1:QsLK0kAbmDXuX/QncFBTETPHCzw5g9hZnzqOPkoB3Yo=
|
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 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
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=
|
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/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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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-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 h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
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/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 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
|
||||||
github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
|
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/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 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
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 h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||||
|
@ -51,7 +51,7 @@ func TestDirectSQLStats(t *testing.T) {
|
|||||||
fStore := folderimpl.ProvideStore(db)
|
fStore := folderimpl.ProvideStore(db)
|
||||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
||||||
folderimpl.ProvideDashboardFolderStore(db), db, featuremgmt.WithFeatures(),
|
folderimpl.ProvideDashboardFolderStore(db), db, featuremgmt.WithFeatures(),
|
||||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||||
|
|
||||||
// create parent folder
|
// create parent folder
|
||||||
|
|
||||||
|
@ -925,7 +925,6 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
|
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
|
||||||
folderWithoutUID := "{ \"title\": \"Folder without UID\"}"
|
|
||||||
folderWithTitleEmpty := "{ \"title\": \"\"}"
|
folderWithTitleEmpty := "{ \"title\": \"\"}"
|
||||||
folderWithInvalidUid := "{ \"uid\": \"::::::::::::\", \"title\": \"Another folder\"}"
|
folderWithInvalidUid := "{ \"uid\": \"::::::::::::\", \"title\": \"Another folder\"}"
|
||||||
folderWithUIDTooLong := "{ \"uid\": \"asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm\", \"title\": \"Third folder\"}"
|
folderWithUIDTooLong := "{ \"uid\": \"asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm\", \"title\": \"Third folder\"}"
|
||||||
@ -964,17 +963,6 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
|
|||||||
expectedMessage: dashboards.ErrFolderAccessDenied.Error(),
|
expectedMessage: dashboards.ErrFolderAccessDenied.Error(),
|
||||||
permissions: []resourcepermissions.SetResourcePermissionCommand{},
|
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",
|
description: "folder creation fails given folder service error %s",
|
||||||
input: folderWithTitleEmpty,
|
input: folderWithTitleEmpty,
|
||||||
|
Loading…
Reference in New Issue
Block a user