K8s: Folders: Fix legacy search (#100393)
This commit is contained in:
committed by
GitHub
parent
ab74852fc9
commit
df84d928e2
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/grafana/dskit/concurrency"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
dashboardalpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/events"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
@@ -42,7 +44,6 @@ import (
|
||||
"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/storage/unified"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -57,7 +58,8 @@ type Service struct {
|
||||
dashboardFolderStore folder.FolderStore
|
||||
features featuremgmt.FeatureToggles
|
||||
accessControl accesscontrol.AccessControl
|
||||
k8sclient folderK8sHandler
|
||||
k8sclient client.K8sHandler
|
||||
dashboardK8sClient client.K8sHandler
|
||||
publicDashboardService publicdashboards.ServiceWrapper
|
||||
// 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.
|
||||
@@ -106,13 +108,14 @@ func ProvideService(
|
||||
ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv))
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesFoldersServiceV2) {
|
||||
k8sHandler := &foldk8sHandler{
|
||||
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
cfg: cfg,
|
||||
restConfigProvider: apiserver.GetRestConfig,
|
||||
recourceClientProvider: unified.GetResourceClient,
|
||||
}
|
||||
k8sHandler := client.NewK8sHandler(
|
||||
cfg,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
apiserver.GetRestConfig,
|
||||
dashboardStore,
|
||||
userService,
|
||||
)
|
||||
|
||||
unifiedStore := ProvideUnifiedStore(k8sHandler, userService)
|
||||
|
||||
@@ -120,6 +123,18 @@ func ProvideService(
|
||||
srv.k8sclient = k8sHandler
|
||||
}
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
dashHandler := client.NewK8sHandler(
|
||||
cfg,
|
||||
request.GetNamespaceMapper(cfg),
|
||||
dashboardalpha1.DashboardResourceInfo.GroupVersionResource(),
|
||||
apiserver.GetRestConfig,
|
||||
dashboardStore,
|
||||
userService,
|
||||
)
|
||||
srv.dashboardK8sClient = dashHandler
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards/database"
|
||||
@@ -494,7 +495,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
})
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService)
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService)
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -580,7 +581,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
|
||||
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService)
|
||||
folderPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService)
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
|
||||
@@ -723,7 +724,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
|
||||
tc.service.store = nestedFolderStore
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, nil, nil, nil, quotaService, nil, publicDashboardFakeService)
|
||||
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, ac, tc.service, tc.service.store, nil, client.MockTestRestConfig{}, nil, quotaService, nil, publicDashboardFakeService)
|
||||
require.NoError(t, err)
|
||||
dashSrv.RegisterDashboardPermissions(dashboardPermissions)
|
||||
|
||||
@@ -1510,8 +1511,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
|
||||
serviceWithFlagOn,
|
||||
nestedFolderStore,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
client.MockTestRestConfig{},
|
||||
nil,
|
||||
quotaService,
|
||||
nil,
|
||||
|
||||
@@ -10,20 +10,15 @@ import (
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/exp/slices"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
dashboardv0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/events"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/infra/slugify"
|
||||
"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"
|
||||
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
||||
@@ -33,31 +28,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
|
||||
"github.com/grafana/grafana/pkg/services/store/entity"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// interface to allow for testing
|
||||
type folderK8sHandler interface {
|
||||
getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
|
||||
getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
|
||||
getNamespace(orgID int64) string
|
||||
getSearcher(ctx context.Context) resource.ResourceClient
|
||||
}
|
||||
|
||||
var _ folderK8sHandler = (*foldk8sHandler)(nil)
|
||||
|
||||
type foldk8sHandler struct {
|
||||
cfg *setting.Cfg
|
||||
namespacer request.NamespaceMapper
|
||||
gvr schema.GroupVersionResource
|
||||
restConfigProvider func(ctx context.Context) *clientrest.Config
|
||||
recourceClientProvider func(ctx context.Context) resource.ResourceClient
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -189,7 +165,7 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S
|
||||
request := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: s.k8sclient.getNamespace(query.OrgID),
|
||||
Namespace: s.k8sclient.GetNamespace(query.OrgID),
|
||||
Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group,
|
||||
Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource,
|
||||
},
|
||||
@@ -228,9 +204,7 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S
|
||||
request.Limit = query.Limit
|
||||
}
|
||||
|
||||
client := s.k8sclient.getSearcher(ctx)
|
||||
|
||||
res, err := client.Search(ctx, request)
|
||||
res, err := s.k8sclient.Search(ctx, query.OrgID, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -279,7 +253,7 @@ func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgI
|
||||
}
|
||||
|
||||
folderkey := &resource.ResourceKey{
|
||||
Namespace: s.k8sclient.getNamespace(orgID),
|
||||
Namespace: s.k8sclient.GetNamespace(orgID),
|
||||
Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group,
|
||||
Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource,
|
||||
}
|
||||
@@ -298,9 +272,7 @@ func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgI
|
||||
},
|
||||
Limit: 100000}
|
||||
|
||||
client := s.k8sclient.getSearcher(ctx)
|
||||
|
||||
res, err := client.Search(ctx, request)
|
||||
res, err := s.k8sclient.Search(ctx, orgID, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -334,7 +306,7 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64
|
||||
}
|
||||
|
||||
folderkey := &resource.ResourceKey{
|
||||
Namespace: s.k8sclient.getNamespace(orgID),
|
||||
Namespace: s.k8sclient.GetNamespace(orgID),
|
||||
Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group,
|
||||
Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource,
|
||||
}
|
||||
@@ -362,9 +334,7 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64
|
||||
request.Options.Fields = append(request.Options.Fields, req...)
|
||||
}
|
||||
|
||||
client := s.k8sclient.getSearcher(ctx)
|
||||
|
||||
res, err := client.Search(ctx, request)
|
||||
res, err := s.k8sclient.Search(ctx, orgID, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -696,14 +666,8 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
|
||||
// we cannot use the dashboard service directly due to circular dependencies,
|
||||
// so either use the search client if the feature is enabled or use the dashboard store
|
||||
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
|
||||
dashboardKey := &resource.ResourceKey{
|
||||
Namespace: s.k8sclient.getNamespace(cmd.OrgID),
|
||||
Group: dashboardv0.DashboardResourceInfo.GroupVersionResource().Group,
|
||||
Resource: dashboardv0.DashboardResourceInfo.GroupVersionResource().Resource,
|
||||
}
|
||||
request := &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: dashboardKey,
|
||||
Labels: []*resource.Requirement{},
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
@@ -715,8 +679,7 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
|
||||
},
|
||||
Limit: 100000}
|
||||
|
||||
client := s.k8sclient.getSearcher(ctx)
|
||||
res, err := client.Search(ctx, request)
|
||||
res, err := s.dashboardK8sClient.Search(ctx, cmd.OrgID, request)
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
@@ -726,13 +689,9 @@ func (s *Service) deleteFromApiServer(ctx context.Context, cmd *folder.DeleteFol
|
||||
return folder.ErrInternal.Errorf("failed to fetch dashboards: %w", err)
|
||||
}
|
||||
dashboardUIDs = make([]string, len(hits.Hits))
|
||||
k8sDeleteClient, created := s.k8sclient.getDashboardClient(ctx, cmd.OrgID)
|
||||
if !created {
|
||||
return folder.ErrInternal.Errorf("failed to create client to get dashboards")
|
||||
}
|
||||
for i, dashboard := range hits.Hits {
|
||||
dashboardUIDs[i] = dashboard.Name
|
||||
err = k8sDeleteClient.Delete(ctx, dashboard.Name, metav1.DeleteOptions{})
|
||||
err = s.dashboardK8sClient.Delete(ctx, dashboard.Name, cmd.OrgID, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return folder.ErrInternal.Errorf("failed to delete child dashboard: %w", err)
|
||||
}
|
||||
@@ -989,43 +948,3 @@ func (s *Service) getDescendantCountsFromApiServer(ctx context.Context, q *folde
|
||||
}
|
||||
return countsMap, nil
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Folder k8s functions
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
||||
func (fk8s *foldk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
||||
cfg := fk8s.restConfigProvider(ctx)
|
||||
if cfg == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
dyn, err := dynamic.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return dyn.Resource(fk8s.gvr).Namespace(fk8s.getNamespace(orgID)), true
|
||||
}
|
||||
|
||||
func (fk8s *foldk8sHandler) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
||||
cfg := fk8s.restConfigProvider(ctx)
|
||||
if cfg == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
dyn, err := dynamic.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return dyn.Resource(dashboardv0.DashboardResourceInfo.GroupVersionResource()).Namespace(fk8s.getNamespace(orgID)), true
|
||||
}
|
||||
|
||||
func (fk8s *foldk8sHandler) getNamespace(orgID int64) string {
|
||||
return fk8s.namespacer(orgID)
|
||||
}
|
||||
|
||||
func (fk8s *foldk8sHandler) getSearcher(ctx context.Context) resource.ResourceClient {
|
||||
return fk8s.recourceClientProvider(ctx)
|
||||
}
|
||||
|
||||
@@ -12,9 +12,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
clientrest "k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
@@ -28,8 +26,10 @@ import (
|
||||
"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/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
@@ -173,23 +173,17 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
Host: folderApiServerMock.URL,
|
||||
}
|
||||
|
||||
f := func(ctx context.Context) resource.ResourceClient {
|
||||
return resourceClientMock{}
|
||||
}
|
||||
|
||||
k8sHandler := &foldk8sHandler{
|
||||
gvr: v0alpha1.FolderResourceInfo.GroupVersionResource(),
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
cfg: cfg,
|
||||
restConfigProvider: restCfgProvider.GetRestConfig,
|
||||
recourceClientProvider: f,
|
||||
}
|
||||
|
||||
userService := &usertest.FakeUserService{
|
||||
ExpectedUser: &user.User{},
|
||||
}
|
||||
|
||||
unifiedStore := ProvideUnifiedStore(k8sHandler, userService)
|
||||
featuresArr := []any{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2}
|
||||
features := featuremgmt.WithFeatures(featuresArr...)
|
||||
|
||||
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
||||
k8sCli := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService)
|
||||
unifiedStore := ProvideUnifiedStore(k8sCli, userService)
|
||||
|
||||
ctx := context.Background()
|
||||
usr := &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
@@ -209,10 +203,6 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
|
||||
}
|
||||
|
||||
featuresArr := []any{
|
||||
featuremgmt.FlagKubernetesFoldersServiceV2}
|
||||
features := featuremgmt.WithFeatures(featuresArr...)
|
||||
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
||||
publicDashboardService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
||||
|
||||
folderService := &Service{
|
||||
@@ -224,7 +214,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
registry: make(map[string]folder.RegistryService),
|
||||
metrics: newFoldersMetrics(nil),
|
||||
tracer: tracing.InitializeTracerForTest(),
|
||||
k8sclient: k8sHandler,
|
||||
k8sclient: k8sCli,
|
||||
dashboardStore: dashboardStore,
|
||||
publicDashboardService: publicDashboardService,
|
||||
}
|
||||
@@ -341,7 +331,6 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
NewTitle: &title,
|
||||
SignedInUser: usr,
|
||||
}
|
||||
|
||||
reqResult, err := folderService.Update(ctx, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, title, reqResult.Title)
|
||||
@@ -358,7 +347,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When deleting folder by uid should not return access denied error - ForceDeleteRules false", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil)
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
|
||||
publicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
err := folderService.Delete(ctx, &folder.DeleteFolderCommand{
|
||||
@@ -428,6 +417,13 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When get folder by ID and uid is an empty string should return folder by id", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{
|
||||
{
|
||||
IsFolder: true,
|
||||
ID: fooFolder.ID, // nolint:staticcheck
|
||||
UID: fooFolder.UID,
|
||||
},
|
||||
}, nil).Once()
|
||||
id := int64(123)
|
||||
emptyString := ""
|
||||
query := &folder.GetFolderQuery{
|
||||
@@ -443,6 +439,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When get folder by non existing ID should return not found error", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
|
||||
id := int64(111111)
|
||||
query := &folder.GetFolderQuery{
|
||||
ID: &id,
|
||||
@@ -456,6 +453,13 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When get folder by Title should return folder", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{
|
||||
{
|
||||
IsFolder: true,
|
||||
ID: fooFolder.ID, // nolint:staticcheck
|
||||
UID: fooFolder.UID,
|
||||
},
|
||||
}, nil).Once()
|
||||
title := "foo"
|
||||
query := &folder.GetFolderQuery{
|
||||
Title: &title,
|
||||
@@ -469,6 +473,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("When get folder by non existing Title should return not found error", func(t *testing.T) {
|
||||
dashboardStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil).Once()
|
||||
title := "does not exists"
|
||||
query := &folder.GetFolderQuery{
|
||||
Title: &title,
|
||||
@@ -506,287 +511,81 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
type resourceClientMock struct{}
|
||||
|
||||
func (r resourceClientMock) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) Search(ctx context.Context, in *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
|
||||
if len(in.Options.Labels) > 0 &&
|
||||
in.Options.Labels[0].Key == utils.LabelKeyDeprecatedInternalID &&
|
||||
in.Options.Labels[0].Operator == "in" &&
|
||||
len(in.Options.Labels[0].Values) > 0 &&
|
||||
in.Options.Labels[0].Values[0] == "123" {
|
||||
return &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "_id",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "foo",
|
||||
Resource: "folders",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("123"),
|
||||
[]byte("folder1"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(in.Options.Fields) > 0 &&
|
||||
in.Options.Fields[0].Key == resource.SEARCH_FIELD_TITLE_PHRASE &&
|
||||
in.Options.Fields[0].Operator == "in" &&
|
||||
len(in.Options.Fields[0].Values) > 0 &&
|
||||
in.Options.Fields[0].Values[0] == "foo" {
|
||||
return &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "_id",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "foo",
|
||||
Resource: "folders",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("123"),
|
||||
[]byte("folder1"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if in.Query == "*test*" {
|
||||
return &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "_id",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Resource: "folders",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("123"),
|
||||
[]byte("testing-123"),
|
||||
[]byte("parent-uid"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(in.Options.Fields) > 0 &&
|
||||
in.Options.Fields[0].Key == resource.SEARCH_FIELD_NAME &&
|
||||
in.Options.Fields[0].Operator == "in" &&
|
||||
len(in.Options.Fields[0].Values) > 0 {
|
||||
rows := []*resource.ResourceTableRow{}
|
||||
for i, row := range in.Options.Fields[0].Values {
|
||||
rows = append(rows, &resource.ResourceTableRow{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: row,
|
||||
Resource: "folders",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte(fmt.Sprintf("%d", i)), // set legacy id as the row id
|
||||
[]byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id
|
||||
[]byte(""),
|
||||
},
|
||||
})
|
||||
}
|
||||
return &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "_id",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: rows,
|
||||
},
|
||||
TotalHits: int64(len(rows)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
if len(in.Options.Fields) > 0 &&
|
||||
in.Options.Fields[0].Key == resource.SEARCH_FIELD_FOLDER &&
|
||||
in.Options.Fields[0].Operator == "in" &&
|
||||
len(in.Options.Fields[0].Values) > 0 {
|
||||
rows := []*resource.ResourceTableRow{}
|
||||
for i, row := range in.Options.Fields[0].Values {
|
||||
rows = append(rows, &resource.ResourceTableRow{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: row,
|
||||
Resource: "folders",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte(fmt.Sprintf("%d", i)), // set legacy id as the row id
|
||||
[]byte(fmt.Sprintf("folder%d", i)), // set title as folder + row id
|
||||
[]byte(""),
|
||||
},
|
||||
})
|
||||
}
|
||||
return &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "_id",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: rows,
|
||||
},
|
||||
TotalHits: int64(len(rows)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// not found
|
||||
return &resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{},
|
||||
}, nil
|
||||
}
|
||||
func (r resourceClientMock) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (r resourceClientMock) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockFoldersK8sCli struct {
|
||||
mock.Mock
|
||||
searcher resourceClientMock
|
||||
}
|
||||
|
||||
func (m *mockFoldersK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
||||
args := m.Called(ctx, orgID)
|
||||
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
|
||||
}
|
||||
|
||||
func (m *mockFoldersK8sCli) getDashboardClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
|
||||
args := m.Called(ctx, orgID)
|
||||
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
|
||||
}
|
||||
|
||||
func (m *mockFoldersK8sCli) getNamespace(orgID int64) string {
|
||||
if orgID == 1 {
|
||||
return "default"
|
||||
}
|
||||
return fmt.Sprintf("orgs-%d", orgID)
|
||||
}
|
||||
|
||||
func (m *mockFoldersK8sCli) getSearcher(ctx context.Context) resource.ResourceClient {
|
||||
return m.searcher
|
||||
}
|
||||
|
||||
func TestSearchFoldersFromApiServer(t *testing.T) {
|
||||
fakeK8sClient := new(mockFoldersK8sCli)
|
||||
service := Service{
|
||||
k8sclient: fakeK8sClient,
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
||||
fakeK8sClient := new(client.MockK8sHandler)
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
CanSaveValue: true,
|
||||
CanViewValue: true,
|
||||
})
|
||||
folderStore := folder.NewFakeStore()
|
||||
folderStore.ExpectedFolder = &folder.Folder{
|
||||
UID: "parent-uid",
|
||||
ID: 2,
|
||||
Title: "parent title",
|
||||
}
|
||||
service := Service{
|
||||
k8sclient: fakeK8sClient,
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
||||
unifiedStore: folderStore,
|
||||
}
|
||||
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
|
||||
user := &user.SignedInUser{OrgID: 1}
|
||||
ctx := identity.WithRequester(context.Background(), user)
|
||||
fakeK8sClient.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
|
||||
|
||||
t.Run("Should search by uids if provided", func(t *testing.T) {
|
||||
t.Run("Should call search with uids, if provided", func(t *testing.T) {
|
||||
fakeK8sClient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group,
|
||||
Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource,
|
||||
},
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_NAME,
|
||||
Operator: string(selection.In),
|
||||
Values: []string{"uid1", "uid2"}, // should only search by uid since it is provided
|
||||
},
|
||||
},
|
||||
Labels: []*resource.Requirement{},
|
||||
},
|
||||
Limit: 100000}).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid1",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("folder0"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid2",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("folder1"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 2,
|
||||
}, nil).Once()
|
||||
query := folder.SearchFoldersQuery{
|
||||
UIDs: []string{"uid1", "uid2"},
|
||||
IDs: []int64{1, 2}, // will ignore these because uid is passed in
|
||||
@@ -821,16 +620,60 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.Equal(t, expectedResult, result)
|
||||
fakeK8sClient.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Search by ID if uids are not provided", func(t *testing.T) {
|
||||
t.Run("Should call search by ID if uids are not provided", func(t *testing.T) {
|
||||
query := folder.SearchFoldersQuery{
|
||||
IDs: []int64{123},
|
||||
SignedInUser: user,
|
||||
}
|
||||
fakeK8sClient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group,
|
||||
Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource,
|
||||
},
|
||||
Fields: []*resource.Requirement{},
|
||||
Labels: []*resource.Requirement{
|
||||
{
|
||||
Key: utils.LabelKeyDeprecatedInternalID,
|
||||
Operator: string(selection.In),
|
||||
Values: []string{"123"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Limit: 100000}).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "foo",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("folder1"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil).Once()
|
||||
|
||||
result, err := service.searchFoldersFromApiServer(ctx, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedResult := model.HitList{
|
||||
{
|
||||
UID: "foo",
|
||||
@@ -844,6 +687,7 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.Equal(t, expectedResult, result)
|
||||
fakeK8sClient.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("Search by title, wildcard should be added to search request (won't match in search mock if not)", func(t *testing.T) {
|
||||
@@ -855,10 +699,45 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
|
||||
Title: "parent title",
|
||||
}
|
||||
service.unifiedStore = fakeFolderStore
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
CanSaveValue: true,
|
||||
CanViewValue: true,
|
||||
})
|
||||
fakeK8sClient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Key: &resource.ResourceKey{
|
||||
Namespace: "default",
|
||||
Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group,
|
||||
Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource,
|
||||
},
|
||||
Fields: []*resource.Requirement{},
|
||||
Labels: []*resource.Requirement{},
|
||||
},
|
||||
Query: "*test*",
|
||||
Fields: dashboardsearch.IncludeFields,
|
||||
Limit: 100000}).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("testing-123"),
|
||||
[]byte("parent-uid"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil).Once()
|
||||
|
||||
query := folder.SearchFoldersQuery{
|
||||
Title: "test",
|
||||
@@ -881,33 +760,26 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
|
||||
},
|
||||
}
|
||||
require.Equal(t, expectedResult, result)
|
||||
fakeK8sClient.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
type mockDashboardCli struct {
|
||||
mock.Mock
|
||||
dynamic.ResourceInterface
|
||||
}
|
||||
|
||||
func (c *mockDashboardCli) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
|
||||
args := c.Called(ctx, name, options)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
fakeK8sClient := new(mockFoldersK8sCli)
|
||||
fakeK8sClient := new(client.MockK8sHandler)
|
||||
fakeK8sClient.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
|
||||
dashboardK8sclient := new(client.MockK8sHandler)
|
||||
fakeFolderStore := folder.NewFakeStore()
|
||||
dashboardStore := dashboards.NewFakeDashboardStore(t)
|
||||
publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t)
|
||||
service := Service{
|
||||
k8sclient: fakeK8sClient,
|
||||
dashboardK8sClient: dashboardK8sclient,
|
||||
unifiedStore: fakeFolderStore,
|
||||
dashboardStore: dashboardStore,
|
||||
publicDashboardService: publicDashboardFakeService,
|
||||
registry: make(map[string]folder.RegistryService),
|
||||
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2),
|
||||
}
|
||||
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
|
||||
user := &user.SignedInUser{OrgID: 1}
|
||||
ctx := identity.WithRequester(context.Background(), user)
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{
|
||||
@@ -969,19 +841,53 @@ func TestDeleteFoldersFromApiServer(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesFoldersServiceV2, featuremgmt.FlagKubernetesCliDashboards)
|
||||
|
||||
t.Run("Should delete dashboards and public dashboards within the folder through k8s if the ff is enabled", func(t *testing.T) {
|
||||
dashboardK8sCli := mockDashboardCli{}
|
||||
dashboardK8sCli.On("Delete", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(nil).Once()
|
||||
fakeK8sClient.On("getDashboardClient", mock.Anything, mock.Anything).Return(&dashboardK8sCli, true)
|
||||
fakeK8sClient.On("getSearcher", mock.Anything).Return(fakeK8sClient)
|
||||
publicDashboardFakeService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"uid1"}).Return(nil).Once()
|
||||
dashboardK8sclient.On("Delete", mock.Anything, "uid1", int64(1), mock.Anything).Return(nil).Once()
|
||||
dashboardK8sclient.On("Search", mock.Anything, int64(1), &resource.ResourceSearchRequest{
|
||||
Options: &resource.ListOptions{
|
||||
Labels: []*resource.Requirement{},
|
||||
Fields: []*resource.Requirement{
|
||||
{
|
||||
Key: resource.SEARCH_FIELD_FOLDER,
|
||||
Operator: string(selection.In),
|
||||
Values: []string{"uid1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Limit: 100000}).Return(&resource.ResourceSearchResponse{
|
||||
Results: &resource.ResourceTable{
|
||||
Columns: []*resource.ResourceTableColumnDefinition{
|
||||
{
|
||||
Name: "title",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
{
|
||||
Name: "folder",
|
||||
Type: resource.ResourceTableColumnDefinition_STRING,
|
||||
},
|
||||
},
|
||||
Rows: []*resource.ResourceTableRow{
|
||||
{
|
||||
Key: &resource.ResourceKey{
|
||||
Name: "uid1",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("folder1"),
|
||||
[]byte(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TotalHits: 1,
|
||||
}, nil).Once()
|
||||
err := service.deleteFromApiServer(ctx, &folder.DeleteFolderCommand{
|
||||
UID: "uid1",
|
||||
OrgID: 1,
|
||||
SignedInUser: user,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
dashboardStore.AssertExpectations(t)
|
||||
publicDashboardFakeService.AssertExpectations(t)
|
||||
dashboardK8sCli.AssertExpectations(t)
|
||||
dashboardK8sclient.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,20 +4,17 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
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/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
|
||||
"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/client"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -26,14 +23,14 @@ import (
|
||||
|
||||
type FolderUnifiedStoreImpl struct {
|
||||
log log.Logger
|
||||
k8sclient folderK8sHandler
|
||||
k8sclient client.K8sHandler
|
||||
userService user.Service
|
||||
}
|
||||
|
||||
// sqlStore implements the store interface.
|
||||
var _ folder.Store = (*FolderUnifiedStoreImpl)(nil)
|
||||
|
||||
func ProvideUnifiedStore(k8sHandler *foldk8sHandler, userService user.Service) *FolderUnifiedStoreImpl {
|
||||
func ProvideUnifiedStore(k8sHandler client.K8sHandler, userService user.Service) *FolderUnifiedStoreImpl {
|
||||
return &FolderUnifiedStoreImpl{
|
||||
k8sclient: k8sHandler,
|
||||
log: log.New("folder-store"),
|
||||
@@ -42,23 +39,11 @@ func ProvideUnifiedStore(k8sHandler *foldk8sHandler, userService user.Service) *
|
||||
}
|
||||
|
||||
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{})
|
||||
out, err := ss.k8sclient.Create(ctx, obj, cmd.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,20 +57,8 @@ func (ss *FolderUnifiedStoreImpl) Create(ctx context.Context, cmd folder.CreateF
|
||||
}
|
||||
|
||||
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{})
|
||||
err := ss.k8sclient.Delete(ctx, uid, orgID, v1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -95,19 +68,7 @@ func (ss *FolderUnifiedStoreImpl) Delete(ctx context.Context, UIDs []string, org
|
||||
}
|
||||
|
||||
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{})
|
||||
obj, err := ss.k8sclient.Get(ctx, cmd.UID, cmd.OrgID, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -133,7 +94,7 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF
|
||||
meta.SetFolder(*cmd.NewParentUID)
|
||||
}
|
||||
|
||||
out, err := client.Update(ctx, updated, v1.UpdateOptions{})
|
||||
out, err := ss.k8sclient.Update(ctx, updated, cmd.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -160,21 +121,7 @@ func (ss *FolderUnifiedStoreImpl) Update(ctx context.Context, cmd folder.UpdateF
|
||||
//
|
||||
// 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{})
|
||||
out, err := ss.k8sclient.Get(ctx, *q.UID, q.OrgID, v1.GetOptions{})
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
} else if err != nil || out == nil {
|
||||
@@ -185,26 +132,12 @@ func (ss *FolderUnifiedStoreImpl) Get(ctx context.Context, q folder.GetFolderQue
|
||||
}
|
||||
|
||||
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{})
|
||||
out, err := ss.k8sclient.Get(ctx, parentUid, q.OrgID, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -226,21 +159,7 @@ func (ss *FolderUnifiedStoreImpl) GetParents(ctx context.Context, q folder.GetPa
|
||||
}
|
||||
|
||||
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{})
|
||||
out, err := ss.k8sclient.List(ctx, q.OrgID, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -328,19 +247,7 @@ func (ss *FolderUnifiedStoreImpl) GetHeight(ctx context.Context, foldrUID string
|
||||
// The full path UIDs of B is "uid1/uid2".
|
||||
// The full path UIDs of A is "uid1".
|
||||
func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFoldersFromStoreQuery) ([]*folder.Folder, error) {
|
||||
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{})
|
||||
out, err := ss.k8sclient.List(ctx, q.OrgID, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -394,21 +301,7 @@ func (ss *FolderUnifiedStoreImpl) GetFolders(ctx context.Context, q folder.GetFo
|
||||
}
|
||||
|
||||
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{})
|
||||
out, err := ss.k8sclient.List(ctx, orgID, v1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -458,21 +351,7 @@ func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string]
|
||||
}
|
||||
|
||||
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")
|
||||
counts, err := ss.k8sclient.Get(ctx, ancestor_uid, orgID, v1.GetOptions{}, "counts")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -502,42 +381,6 @@ func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCount
|
||||
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
|
||||
}
|
||||
|
||||
func computeFullPath(parents []*folder.Folder) (string, string) {
|
||||
fullpath := make([]string, len(parents))
|
||||
fullpathUIDs := make([]string, len(parents))
|
||||
|
||||
Reference in New Issue
Block a user