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 |
|
||||
| `kubernetesRestore` | Allow restoring objects in k8s |
|
||||
| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s |
|
||||
| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s |
|
||||
| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs |
|
||||
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
|
||||
| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query |
|
||||
|
@ -113,6 +113,7 @@ export interface FeatureToggles {
|
||||
kubernetesCliDashboards?: boolean;
|
||||
kubernetesRestore?: boolean;
|
||||
kubernetesFolders?: boolean;
|
||||
kubernetesFoldersServiceV2?: boolean;
|
||||
grafanaAPIServerTestingWithExperimentalAPIs?: boolean;
|
||||
datasourceQueryTypes?: boolean;
|
||||
queryService?: boolean;
|
||||
|
@ -834,7 +834,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
|
||||
quotaService := quotatest.New(false, nil)
|
||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashboardStore, folderStore, db, features,
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
if dashboardService == nil {
|
||||
dashboardService, err = service.ProvideDashboardServiceImpl(
|
||||
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
|
||||
|
@ -57,7 +57,7 @@ func (hs *HTTPServer) registerFolderAPI(apiRoute routing.RouteRegister, authoriz
|
||||
folderPermissionRoute.Post("/", authorize(accesscontrol.EvalPermission(dashboards.ActionFoldersPermissionsWrite, uidScope)), routing.Wrap(hs.UpdateFolderPermissions))
|
||||
})
|
||||
})
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) {
|
||||
if hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFolders) && !hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
// Use k8s client to implement legacy API
|
||||
handler := newFolderK8sHandler(hs)
|
||||
folderRoute.Post("/", handler.createFolder)
|
||||
|
@ -461,7 +461,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
|
||||
actionSets := resourcepermissions.NewActionSetService(features)
|
||||
fStore := folderimpl.ProvideStore(sc.db)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
||||
folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderStore, sc.db, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
sc.cfg, acdb.ProvideService(sc.db), actionSets, localcache.ProvideService(),
|
||||
features, tracing.InitializeTracerForTest(), zanzana.NewNoopClient(), sc.db, permreg.ProvidePermissionRegistry(), nil, folderServiceWithFlagOn,
|
||||
|
@ -52,6 +52,11 @@ func LegacyUpdateCommandToUnstructured(obj *unstructured.Unstructured, cmd *fold
|
||||
if cmd.NewDescription != nil {
|
||||
spec["description"] = cmd.NewDescription
|
||||
}
|
||||
if cmd.NewParentUID != nil {
|
||||
if err := setParentUID(obj, *cmd.NewParentUID); err != nil {
|
||||
return &unstructured.Unstructured{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
}
|
||||
|
||||
// List must return all folders
|
||||
hits, err := s.service.GetFolders(ctx, folder.GetFoldersQuery{
|
||||
hits, err := s.service.GetFoldersLegacy(ctx, folder.GetFoldersQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgId,
|
||||
// TODO: enable pagination
|
||||
@ -133,7 +133,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dto, err := s.service.Get(ctx, &folder.GetFolderQuery{
|
||||
dto, err := s.service.GetLegacy(ctx, &folder.GetFolderQuery{
|
||||
SignedInUser: user,
|
||||
UID: &name,
|
||||
OrgID: info.OrgID,
|
||||
@ -186,7 +186,7 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
|
||||
parent := accessor.GetFolder()
|
||||
|
||||
out, err := s.service.Create(ctx, &folder.CreateFolderCommand{
|
||||
out, err := s.service.CreateLegacy(ctx, &folder.CreateFolderCommand{
|
||||
SignedInUser: user,
|
||||
UID: p.Name,
|
||||
Title: p.Spec.Title,
|
||||
@ -285,7 +285,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
oldParent := mOld.GetFolder()
|
||||
newParent := mNew.GetFolder()
|
||||
if oldParent != newParent {
|
||||
_, err = s.service.Move(ctx, &folder.MoveFolderCommand{
|
||||
_, err = s.service.MoveLegacy(ctx, &folder.MoveFolderCommand{
|
||||
SignedInUser: user,
|
||||
UID: name,
|
||||
OrgID: info.OrgID,
|
||||
@ -312,7 +312,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
_, err = s.service.Update(ctx, cmd)
|
||||
_, err = s.service.UpdateLegacy(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@ -340,7 +340,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
|
||||
if !ok {
|
||||
return v, false, fmt.Errorf("expected a folder response from Get")
|
||||
}
|
||||
err = s.service.Delete(ctx, &folder.DeleteFolderCommand{
|
||||
err = s.service.DeleteLegacy(ctx, &folder.DeleteFolderCommand{
|
||||
UID: name,
|
||||
OrgID: info.OrgID,
|
||||
SignedInUser: user,
|
||||
|
@ -66,6 +66,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
||||
) *FolderAPIBuilder {
|
||||
if !featuremgmt.AnyEnabled(features,
|
||||
featuremgmt.FlagKubernetesFolders,
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2,
|
||||
featuremgmt.FlagGrafanaAPIServerTestingWithExperimentalAPIs,
|
||||
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||
featuremgmt.FlagProvisioning) {
|
||||
|
@ -49,7 +49,7 @@ func ProvideFolderPermissions(
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||
fService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashboardStore, folderStore, sqlStore, features,
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
acSvc := acimpl.ProvideOSSService(
|
||||
cfg, acdb.ProvideService(sqlStore), actionSets, localcache.ProvideService(),
|
||||
|
@ -50,7 +50,7 @@ func TestIntegrationAuthorize(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, sql, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||
require.NoError(t, err)
|
||||
|
@ -62,7 +62,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
folderSvc := folderimpl.ProvideService(fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, sql, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||
require.NoError(t, err)
|
||||
@ -244,7 +244,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(sql)
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sql)
|
||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
||||
folderStore, sql, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderStore, sql, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), accesscontrolmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||
require.NoError(t, err)
|
||||
|
@ -300,7 +300,7 @@ func TestIntegrationDashboardInheritedFolderRBAC(t *testing.T) {
|
||||
})
|
||||
|
||||
folderStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderSvc := folderimpl.ProvideService(folderStore, mock.New(), bus.ProvideBus(tracer), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(sqlStore), sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
parentUID := ""
|
||||
for i := 0; ; i++ {
|
||||
|
@ -891,7 +891,7 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
|
||||
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
@ -1010,7 +1010,7 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
|
||||
folderServiceWithFlagOn := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore,
|
||||
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderStore, sqlStore, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
user := &user.SignedInUser{
|
||||
OrgID: 1,
|
||||
|
@ -372,13 +372,15 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
|
||||
|
||||
// Validate folder
|
||||
if dash.FolderUID != "" {
|
||||
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||
// nolint:staticcheck
|
||||
dash.FolderID = folder.ID
|
||||
}
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||
// nolint:staticcheck
|
||||
dash.FolderID = folder.ID
|
||||
} else if dash.FolderID != 0 { // nolint:staticcheck
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||
// nolint:staticcheck
|
||||
|
@ -101,6 +101,7 @@ func TestIntegrationDashboardServiceZanzana(t *testing.T) {
|
||||
db,
|
||||
featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders),
|
||||
supportbundlestest.NewFakeBundleService(),
|
||||
cfg,
|
||||
nil,
|
||||
tracing.InitializeTracerForTest(),
|
||||
)
|
||||
|
@ -714,6 +714,12 @@ var (
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
},
|
||||
{
|
||||
Name: "kubernetesFoldersServiceV2",
|
||||
Description: "Use the Folders Service V2, and route Folder Service requests to k8s",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaSearchAndStorageSquad,
|
||||
},
|
||||
{
|
||||
Name: "grafanaAPIServerTestingWithExperimentalAPIs",
|
||||
Description: "Facilitate integration testing of experimental APIs",
|
||||
|
@ -94,6 +94,7 @@ kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,fals
|
||||
kubernetesCliDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
kubernetesRestore,experimental,@grafana/grafana-app-platform-squad,false,false,false
|
||||
kubernetesFolders,experimental,@grafana/search-and-storage,false,false,false
|
||||
kubernetesFoldersServiceV2,experimental,@grafana/search-and-storage,false,false,false
|
||||
grafanaAPIServerTestingWithExperimentalAPIs,experimental,@grafana/search-and-storage,false,false,false
|
||||
datasourceQueryTypes,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
queryService,experimental,@grafana/grafana-app-platform-squad,false,true,false
|
||||
|
|
@ -387,6 +387,10 @@ const (
|
||||
// Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s
|
||||
FlagKubernetesFolders = "kubernetesFolders"
|
||||
|
||||
// FlagKubernetesFoldersServiceV2
|
||||
// Use the Folders Service V2, and route Folder Service requests to k8s
|
||||
FlagKubernetesFoldersServiceV2 = "kubernetesFoldersServiceV2"
|
||||
|
||||
// FlagGrafanaAPIServerTestingWithExperimentalAPIs
|
||||
// Facilitate integration testing of experimental APIs
|
||||
FlagGrafanaAPIServerTestingWithExperimentalAPIs = "grafanaAPIServerTestingWithExperimentalAPIs"
|
||||
|
@ -2081,6 +2081,18 @@
|
||||
"codeowner": "@grafana/search-and-storage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesFoldersServiceV2",
|
||||
"resourceVersion": "1735336477446",
|
||||
"creationTimestamp": "2024-12-27T21:54:37Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use the Folders Service V2, and route Folder Service requests to k8s",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/search-and-storage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "kubernetesPlaylists",
|
||||
|
@ -18,12 +18,14 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/events"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@ -34,6 +36,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@ -41,12 +44,14 @@ const FULLPATH_SEPARATOR = "/"
|
||||
|
||||
type Service struct {
|
||||
store folder.Store
|
||||
unifiedStore folder.Store
|
||||
db db.DB
|
||||
log *slog.Logger
|
||||
dashboardStore dashboards.Store
|
||||
dashboardFolderStore folder.FolderStore
|
||||
features featuremgmt.FeatureToggles
|
||||
accessControl accesscontrol.AccessControl
|
||||
k8sclient folderK8sHandler
|
||||
// bus is currently used to publish event in case of folder full path change.
|
||||
// For example when a folder is moved to another folder or when a folder is renamed.
|
||||
bus bus.Bus
|
||||
@ -66,14 +71,24 @@ func ProvideService(
|
||||
db db.DB, // DB for the (new) nested folder store
|
||||
features featuremgmt.FeatureToggles,
|
||||
supportBundles supportbundles.Service,
|
||||
cfg *setting.Cfg,
|
||||
r prometheus.Registerer,
|
||||
tracer tracing.Tracer,
|
||||
) *Service {
|
||||
k8sHandler := &foldk8sHandler{
|
||||
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
unifiedStore := ProvideUnifiedStore(cfg)
|
||||
|
||||
srv := &Service{
|
||||
log: slog.Default().With("logger", "folder-service"),
|
||||
dashboardStore: dashboardStore,
|
||||
dashboardFolderStore: folderStore,
|
||||
store: store,
|
||||
unifiedStore: unifiedStore,
|
||||
features: features,
|
||||
accessControl: ac,
|
||||
bus: bus,
|
||||
@ -81,6 +96,7 @@ func ProvideService(
|
||||
registry: make(map[string]folder.RegistryService),
|
||||
metrics: newFoldersMetrics(r),
|
||||
tracer: tracer,
|
||||
k8sclient: k8sHandler,
|
||||
}
|
||||
srv.DBMigration(db)
|
||||
|
||||
@ -138,6 +154,13 @@ func (s *Service) DBMigration(db db.DB) {
|
||||
}
|
||||
|
||||
func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.getFoldersFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetFoldersLegacy(ctx, q)
|
||||
}
|
||||
|
||||
func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||
if q.SignedInUser == nil {
|
||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
||||
}
|
||||
@ -190,6 +213,13 @@ func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.getFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetLegacy(ctx, q)
|
||||
}
|
||||
|
||||
func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
if q.SignedInUser == nil {
|
||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
||||
}
|
||||
@ -278,13 +308,13 @@ func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Fo
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) {
|
||||
f, err = s.setFullpath(ctx, f, q.SignedInUser)
|
||||
f, err = s.setFullpath(ctx, f, q.SignedInUser, true)
|
||||
}
|
||||
|
||||
return f, err
|
||||
}
|
||||
|
||||
func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identity.Requester) (*folder.Folder, error) {
|
||||
func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identity.Requester, forceLegacy bool) (*folder.Folder, error) {
|
||||
// #TODO is some kind of intermediate conversion required as is the case with user id where
|
||||
// it gets parsed using UserIdentifier(). Also is there some kind of validation taking place as
|
||||
// part of the parsing?
|
||||
@ -297,10 +327,19 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identi
|
||||
|
||||
// Fetch the parent since the permissions for fetching the newly created folder
|
||||
// are not yet present for the user--this requires a call to ClearUserPermissionCache
|
||||
parents, err := s.GetParents(ctx, folder.GetParentsQuery{
|
||||
UID: f.UID,
|
||||
OrgID: f.OrgID,
|
||||
})
|
||||
var parents []*folder.Folder
|
||||
var err error
|
||||
if forceLegacy {
|
||||
parents, err = s.GetParentsLegacy(ctx, folder.GetParentsQuery{
|
||||
UID: f.UID,
|
||||
OrgID: f.OrgID,
|
||||
})
|
||||
} else {
|
||||
parents, err = s.GetParents(ctx, folder.GetParentsQuery{
|
||||
UID: f.UID,
|
||||
OrgID: f.OrgID,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -319,6 +358,13 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, user identi
|
||||
}
|
||||
|
||||
func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.getChildrenFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetChildrenLegacy(ctx, q)
|
||||
}
|
||||
|
||||
func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
defer func(t time.Time) {
|
||||
parent := q.UID
|
||||
if q.UID != folder.SharedWithMeFolderUID {
|
||||
@ -332,7 +378,7 @@ func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) (
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) && q.UID == folder.SharedWithMeFolderUID {
|
||||
return s.GetSharedWithMe(ctx, q)
|
||||
return s.GetSharedWithMe(ctx, q, true)
|
||||
}
|
||||
|
||||
if q.UID == "" {
|
||||
@ -460,14 +506,19 @@ func (s *Service) getRootFolders(ctx context.Context, q *folder.GetChildrenQuery
|
||||
}
|
||||
|
||||
// GetSharedWithMe returns folders available to user, which cannot be accessed from the root folders
|
||||
func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuery, forceLegacy bool) ([]*folder.Folder, error) {
|
||||
start := time.Now()
|
||||
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q)
|
||||
availableNonRootFolders, err := s.getAvailableNonRootFolders(ctx, q, forceLegacy)
|
||||
if err != nil {
|
||||
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
||||
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders to which the user has explicit access: %w", err)
|
||||
}
|
||||
rootFolders, err := s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
|
||||
var rootFolders []*folder.Folder
|
||||
if forceLegacy {
|
||||
rootFolders, err = s.GetChildrenLegacy(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
|
||||
} else {
|
||||
rootFolders, err = s.GetChildren(ctx, &folder.GetChildrenQuery{UID: "", OrgID: q.OrgID, SignedInUser: q.SignedInUser, Permission: q.Permission})
|
||||
}
|
||||
if err != nil {
|
||||
s.metrics.sharedWithMeFetchFoldersRequestsDuration.WithLabelValues("failure").Observe(time.Since(start).Seconds())
|
||||
return nil, folder.ErrInternal.Errorf("failed to fetch root folders to which the user has access: %w", err)
|
||||
@ -478,7 +529,7 @@ func (s *Service) GetSharedWithMe(ctx context.Context, q *folder.GetChildrenQuer
|
||||
return availableNonRootFolders, nil
|
||||
}
|
||||
|
||||
func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetChildrenQuery, forceLegacy bool) ([]*folder.Folder, error) {
|
||||
permissions := q.SignedInUser.GetPermissions()
|
||||
var folderPermissions []string
|
||||
if q.Permission == dashboardaccess.PERMISSION_EDIT {
|
||||
@ -507,13 +558,25 @@ func (s *Service) getAvailableNonRootFolders(ctx context.Context, q *folder.GetC
|
||||
return nonRootFolders, nil
|
||||
}
|
||||
|
||||
dashFolders, err := s.GetFolders(ctx, folder.GetFoldersQuery{
|
||||
UIDs: folderUids,
|
||||
OrgID: q.OrgID,
|
||||
SignedInUser: q.SignedInUser,
|
||||
OrderByTitle: true,
|
||||
WithFullpathUIDs: true,
|
||||
})
|
||||
var dashFolders []*folder.Folder
|
||||
var err error
|
||||
if forceLegacy {
|
||||
dashFolders, err = s.GetFoldersLegacy(ctx, folder.GetFoldersQuery{
|
||||
UIDs: folderUids,
|
||||
OrgID: q.OrgID,
|
||||
SignedInUser: q.SignedInUser,
|
||||
OrderByTitle: true,
|
||||
WithFullpathUIDs: true,
|
||||
})
|
||||
} else {
|
||||
dashFolders, err = s.GetFolders(ctx, folder.GetFoldersQuery{
|
||||
UIDs: folderUids,
|
||||
OrgID: q.OrgID,
|
||||
SignedInUser: q.SignedInUser,
|
||||
OrderByTitle: true,
|
||||
WithFullpathUIDs: true,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, folder.ErrInternal.Errorf("failed to fetch subfolders: %w", err)
|
||||
}
|
||||
@ -565,6 +628,13 @@ func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*fo
|
||||
}
|
||||
|
||||
func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.getParentsFromApiServer(ctx, q)
|
||||
}
|
||||
return s.GetParentsLegacy(ctx, q)
|
||||
}
|
||||
|
||||
func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
||||
if !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) || q.UID == accesscontrol.GeneralFolderUID {
|
||||
return nil, nil
|
||||
}
|
||||
@ -591,6 +661,13 @@ func (s *Service) getFolderByTitle(ctx context.Context, orgID int64, title strin
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.createOnApiServer(ctx, cmd)
|
||||
}
|
||||
return s.CreateLegacy(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
if cmd.SignedInUser == nil || cmd.SignedInUser.IsNil() {
|
||||
return nil, folder.ErrBadRequest.Errorf("missing signed in user")
|
||||
}
|
||||
@ -700,13 +777,23 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (
|
||||
}
|
||||
|
||||
if s.features.IsEnabled(ctx, featuremgmt.FlagKubernetesFolders) {
|
||||
f, err = s.setFullpath(ctx, f, user)
|
||||
f, err = s.setFullpath(ctx, f, user, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.updateOnApiServer(ctx, cmd)
|
||||
}
|
||||
return s.UpdateLegacy(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "folder.Update")
|
||||
defer span.End()
|
||||
|
||||
@ -839,6 +926,13 @@ func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int6
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.deleteFromApiServer(ctx, cmd)
|
||||
}
|
||||
return s.DeleteLegacy(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
|
||||
if cmd.SignedInUser == nil {
|
||||
return folder.ErrBadRequest.Errorf("missing signed in user")
|
||||
}
|
||||
@ -922,6 +1016,13 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
|
||||
}
|
||||
|
||||
func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.moveOnApiServer(ctx, cmd)
|
||||
}
|
||||
return s.MoveLegacy(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *Service) MoveLegacy(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
|
||||
ctx, span := s.tracer.Start(ctx, "folder.Move")
|
||||
defer span.End()
|
||||
|
||||
@ -1142,6 +1243,14 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold
|
||||
}
|
||||
|
||||
func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
return s.getDescendantCountsFromApiServer(ctx, q)
|
||||
}
|
||||
|
||||
return s.GetDescendantCountsLegacy(ctx, q)
|
||||
}
|
||||
|
||||
func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||
if q.SignedInUser == nil {
|
||||
return nil, folder.ErrBadRequest.Errorf("missing signed-in user")
|
||||
}
|
||||
|
@ -62,10 +62,10 @@ func TestIntegrationProvideFolderService(t *testing.T) {
|
||||
}
|
||||
t.Run("should register scope resolvers", func(t *testing.T) {
|
||||
ac := acmock.New()
|
||||
db, _ := db.InitTestDBWithCfg(t)
|
||||
db, cfg := db.InitTestDBWithCfg(t)
|
||||
store := ProvideStore(db)
|
||||
ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), nil, nil, db,
|
||||
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 2)
|
||||
})
|
||||
|
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) {
|
||||
return s.ExpectedFolders, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.Folder, error) {
|
||||
return s.ExpectedFolders, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
||||
return s.ExpectedFolders, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
|
||||
return s.ExpectedFolders, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
|
||||
return s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
|
||||
return s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) MoveLegacy(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
|
||||
return s.ExpectedFolder, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) RegisterService(service folder.RegistryService) error {
|
||||
return s.ExpectedError
|
||||
@ -51,7 +75,13 @@ func (s *FakeService) RegisterService(service folder.RegistryService) error {
|
||||
func (s *FakeService) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||
return s.ExpectedDescendantCounts, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
|
||||
return s.ExpectedDescendantCounts, s.ExpectedError
|
||||
}
|
||||
|
||||
func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||
return s.ExpectedFolders, s.ExpectedError
|
||||
}
|
||||
func (s *FakeService) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
|
||||
return s.ExpectedFolders, s.ExpectedError
|
||||
}
|
||||
|
@ -5,12 +5,10 @@ import (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
// GetChildren returns an array containing all child folders.
|
||||
GetChildren(ctx context.Context, q *GetChildrenQuery) ([]*Folder, error)
|
||||
// GetParents returns an array containing add parent folders if nested folders are enabled
|
||||
// otherwise it returns an empty array
|
||||
GetParents(ctx context.Context, q GetParentsQuery) ([]*Folder, error)
|
||||
RegisterService(service RegistryService) error
|
||||
|
||||
Create(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error)
|
||||
CreateLegacy(ctx context.Context, cmd *CreateFolderCommand) (*Folder, error)
|
||||
|
||||
// GetFolder takes a GetFolderCommand and returns a folder matching the
|
||||
// request. One of UID, ID or Title must be included. If multiple values
|
||||
@ -20,21 +18,39 @@ type Service interface {
|
||||
// If ParentUID is not set then the folder will be fetched from the root level.
|
||||
// If WithFullpath is true it computes also the full path of a folder.
|
||||
Get(ctx context.Context, q *GetFolderQuery) (*Folder, error)
|
||||
GetLegacy(ctx context.Context, q *GetFolderQuery) (*Folder, error)
|
||||
|
||||
// Update is used to update a folder's UID, Title and Description. To change
|
||||
// a folder's parent folder, use Move.
|
||||
Update(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
|
||||
UpdateLegacy(ctx context.Context, cmd *UpdateFolderCommand) (*Folder, error)
|
||||
|
||||
Delete(ctx context.Context, cmd *DeleteFolderCommand) error
|
||||
DeleteLegacy(ctx context.Context, cmd *DeleteFolderCommand) error
|
||||
|
||||
// Move changes a folder's parent folder to the requested new parent.
|
||||
Move(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
|
||||
RegisterService(service RegistryService) error
|
||||
MoveLegacy(ctx context.Context, cmd *MoveFolderCommand) (*Folder, error)
|
||||
|
||||
// GetFolders returns org folders that are accessible by the signed in user by their UIDs.
|
||||
// If WithFullpath is true it computes also the full path of a folder.
|
||||
// The full path is a string that contains the titles of all parent folders separated by a slash.
|
||||
// If a folder contains a slash in its title, it is escaped with a backslash.
|
||||
// If FullpathUIDs is true it computes a string that contains the UIDs of all parent folders separated by slash.
|
||||
GetFolders(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
|
||||
GetFoldersLegacy(ctx context.Context, q GetFoldersQuery) ([]*Folder, error)
|
||||
|
||||
// GetChildren returns an array containing all child folders.
|
||||
GetChildren(ctx context.Context, q *GetChildrenQuery) ([]*Folder, error)
|
||||
GetChildrenLegacy(ctx context.Context, q *GetChildrenQuery) ([]*Folder, error)
|
||||
|
||||
// GetParents returns an array containing add parent folders if nested folders are enabled
|
||||
// otherwise it returns an empty array
|
||||
GetParents(ctx context.Context, q GetParentsQuery) ([]*Folder, error)
|
||||
GetParentsLegacy(ctx context.Context, q GetParentsQuery) ([]*Folder, error)
|
||||
|
||||
GetDescendantCounts(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
|
||||
GetDescendantCountsLegacy(ctx context.Context, q *GetDescendantCountsQuery) (DescendantCounts, error)
|
||||
}
|
||||
|
||||
// FolderStore is a folder store.
|
||||
|
@ -336,7 +336,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
|
||||
store := folderimpl.ProvideStore(sc.sqlStore)
|
||||
s := folderimpl.ProvideService(store, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
t.Logf("Creating folder with title and UID %q", title)
|
||||
ctx := identity.WithRequester(context.Background(), &sc.user)
|
||||
folder, err := s.Create(ctx, &folder.CreateFolderCommand{
|
||||
@ -473,7 +473,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
guardian.InitAccessControlGuardian(cfg, ac, dashService)
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderSrv := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracer), dashboardStore, folderStore, sqlStore,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
service := LibraryElementService{
|
||||
Cfg: cfg,
|
||||
features: featuremgmt.WithFeatures(),
|
||||
|
@ -755,7 +755,7 @@ func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
|
||||
fStore := folderimpl.ProvideStore(sc.sqlStore)
|
||||
s := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sc.sqlStore,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
t.Logf("Creating folder with title and UID %q", title)
|
||||
ctx := identity.WithRequester(context.Background(), sc.user)
|
||||
@ -840,7 +840,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
|
||||
folderService := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac)
|
||||
service := LibraryPanelService{
|
||||
|
@ -1920,7 +1920,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore, sqlStore,
|
||||
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
store := store.DBstore{
|
||||
Logger: log,
|
||||
SQLStore: sqlStore,
|
||||
|
@ -1604,7 +1604,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders)
|
||||
fStore := folderimpl.ProvideStore(sqlStore)
|
||||
folderService := folderimpl.ProvideService(fStore, ac, inProcBus, dashboardStore, folderStore, sqlStore,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
ruleService := createAlertRuleService(t, folderService)
|
||||
var orgID int64 = 1
|
||||
|
@ -30,7 +30,7 @@ func SetupFolderService(tb testing.TB, cfg *setting.Cfg, db db.DB, dashboardStor
|
||||
tb.Helper()
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
return folderimpl.ProvideService(fStore, ac, bus, dashboardStore, folderStore, db,
|
||||
features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
}
|
||||
|
||||
func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.DashboardFolderStoreImpl, cfg *setting.Cfg) (*dashboardservice.DashboardServiceImpl, dashboards.Store) {
|
||||
|
@ -1395,7 +1395,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(testDB)
|
||||
folderPermissions := acmock.NewMockedPermissionsService()
|
||||
folderStore := folderimpl.ProvideDashboardFolderStore(testDB)
|
||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, testDB, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderSvc := folderimpl.ProvideService(fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, testDB, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, &actest.FakePermissionsService{}, ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotatest.New(false, nil), nil)
|
||||
require.NoError(t, err)
|
||||
|
@ -494,7 +494,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
|
||||
ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient())
|
||||
folderSvc := folderimpl.ProvideService(fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()),
|
||||
dashStore, folderStore, sqlStore, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
_, err = dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), acmock.NewMockedPermissionsService(),
|
||||
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, quotaService, nil)
|
||||
|
||||
|
@ -823,7 +823,7 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
|
||||
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
||||
folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
folderimpl.ProvideDashboardFolderStore(db), db, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
// create parent folder
|
||||
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
|
||||
|
@ -80,7 +80,7 @@ func setupBenchMark(b *testing.B, usr user.SignedInUser, features featuremgmt.Fe
|
||||
|
||||
fStore := folderimpl.ProvideStore(store)
|
||||
folderSvc := folderimpl.ProvideService(fStore, mock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardWriteStore, folderimpl.ProvideDashboardFolderStore(store),
|
||||
store, features, supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
store, features, supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
origNewGuardian := guardian.New
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true})
|
||||
|
@ -100,6 +100,8 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
|
||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
@ -559,6 +561,10 @@ github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2 h1:fV6IgVtViXcYZ4VqTAMuVBTLuGA
|
||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.2/go.mod h1:Cbh94bfL5o6mUSaHFiOkx4r4CRKlo/DJLx4dPL8XrE0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.261.0 h1:pGGpPbKRWZcLxwNATEiVDhILbYGYwlWOEXFLhmUkBMo=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.261.0/go.mod h1:QsLK0kAbmDXuX/QncFBTETPHCzw5g9hZnzqOPkoB3Yo=
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2 h1:2H9x4q53pkfUGtSNYD1qSBpNnxrFgylof/TYADb5xMI=
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20240813192817-1b0e6b5c09b2/go.mod h1:gBLBniiSUQvyt4LRrpIeysj8Many0DV+hdUKifRE0Ec=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435/go.mod h1:8cz+z0i57IjN6MYmu/zZQdCg9CQcsnEHbaJBBEf3KQo=
|
||||
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
|
||||
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
|
||||
@ -775,6 +781,8 @@ github.com/mithrandie/ternary v1.1.1 h1:k/joD6UGVYxHixYmSR8EGgDFNONBMqyD373xT4QR
|
||||
github.com/mithrandie/ternary v1.1.1/go.mod h1:0D9Ba3+09K2TdSZO7/bFCC0GjSXetCvYuYq0u8FY/1g=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -792,6 +800,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
|
||||
github.com/natefinch/wrap v0.2.0 h1:IXzc/pw5KqxJv55gV0lSOcKHYuEZPGbQrOOXr/bamRk=
|
||||
github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm4/KAS8=
|
||||
@ -1546,6 +1556,10 @@ k8s.io/component-base v0.32.0 h1:d6cWHZkCiiep41ObYQS6IcgzOUQUNpywm39KVYaUqzU=
|
||||
k8s.io/component-base v0.32.0/go.mod h1:JLG2W5TUxUu5uDyKiH2R/7NnxJo1HlPoRIIbVLkK5eM=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.32.0 h1:jwOfunHIrcdYl5FRcA+uUKKtg6qiqoPCwmS2T3XTYL4=
|
||||
k8s.io/kms v0.32.0/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM=
|
||||
k8s.io/kube-aggregator v0.32.0 h1:5ZyMW3QwAbmkasQrROcpa5we3et938DQuyUYHeXSPao=
|
||||
k8s.io/kube-aggregator v0.32.0/go.mod h1:6OKivf6Ypx44qu2v1ZUMrxH8kRp/8LKFKeJU72J18lU=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
|
@ -51,7 +51,7 @@ func TestDirectSQLStats(t *testing.T) {
|
||||
fStore := folderimpl.ProvideStore(db)
|
||||
folderSvc := folderimpl.ProvideService(fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore,
|
||||
folderimpl.ProvideDashboardFolderStore(db), db, featuremgmt.WithFeatures(),
|
||||
supportbundlestest.NewFakeBundleService(), nil, tracing.InitializeTracerForTest())
|
||||
supportbundlestest.NewFakeBundleService(), cfg, nil, tracing.InitializeTracerForTest())
|
||||
|
||||
// create parent folder
|
||||
|
||||
|
@ -925,7 +925,6 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
|
||||
}
|
||||
|
||||
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
|
||||
folderWithoutUID := "{ \"title\": \"Folder without UID\"}"
|
||||
folderWithTitleEmpty := "{ \"title\": \"\"}"
|
||||
folderWithInvalidUid := "{ \"uid\": \"::::::::::::\", \"title\": \"Another folder\"}"
|
||||
folderWithUIDTooLong := "{ \"uid\": \"asdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnmasdfghjklqwertyuiopzxcvbnm\", \"title\": \"Third folder\"}"
|
||||
@ -964,17 +963,6 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) {
|
||||
expectedMessage: dashboards.ErrFolderAccessDenied.Error(),
|
||||
permissions: []resourcepermissions.SetResourcePermissionCommand{},
|
||||
},
|
||||
{
|
||||
// #TODO This test case doesn't set up the conditions it describes. We should have created a folder with the same UID before
|
||||
// creating a second one and failing to do so successfully.
|
||||
description: "folder creation fails given folder service error %s",
|
||||
input: folderWithoutUID,
|
||||
expectedCode: http.StatusConflict,
|
||||
// expectedMessage: dashboards.ErrFolderWithSameUIDExists.Error(),
|
||||
expectedFolderSvcError: dashboards.ErrFolderWithSameUIDExists,
|
||||
createSecondRecord: true,
|
||||
permissions: folderCreatePermission,
|
||||
},
|
||||
{
|
||||
description: "folder creation fails given folder service error %s",
|
||||
input: folderWithTitleEmpty,
|
||||
|
Loading…
Reference in New Issue
Block a user