K8s: Folders: Fix legacy search (#100393)

This commit is contained in:
Stephanie Hingtgen 2025-02-11 12:14:25 -07:00 committed by GitHub
parent ab74852fc9
commit df84d928e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 416 additions and 661 deletions

View File

@ -30,6 +30,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/apiserver/client"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
@ -835,7 +836,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
if dashboardService == nil {
dashboardService, err = service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions,
ac, folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, nil,
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(t, err)
dashboardService.(dashboards.PermissionsRegistrationService).RegisterDashboardPermissions(dashboardPermissions)
@ -843,7 +844,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions,
ac, folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, nil,
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(t, err)

View File

@ -27,6 +27,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/permreg"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -472,7 +473,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
sc.cfg, dashStore, folderStore,
features, folderPermissions, ac,
folderServiceWithFlagOn, fStore, nil, nil, nil, nil, quotaSrv, nil, nil,
folderServiceWithFlagOn, fStore, nil, client.MockTestRestConfig{}, nil, quotaSrv, nil, nil,
)
require.NoError(b, err)

View File

@ -1,6 +1,7 @@
package dashboard
import (
"context"
"encoding/json"
"net/http"
"net/url"
@ -9,6 +10,7 @@ import (
"strconv"
"strings"
"github.com/grafana/grafana/pkg/storage/unified"
"github.com/grafana/grafana/pkg/storage/unified/search"
"go.opentelemetry.io/otel/trace"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -32,12 +34,12 @@ import (
// The DTO returns everything the UI needs in a single request
type SearchHandler struct {
log log.Logger
client resource.ResourceIndexClient
client func(context.Context) resource.ResourceIndexClient
tracer trace.Tracer
}
func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler {
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, client, legacyDashboardSearcher)
func NewSearchHandler(tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler {
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, unified.GetResourceClient, legacyDashboardSearcher)
return &SearchHandler{
client: searchClient,
log: log.New("grafana-apiserver.dashboards.search"),
@ -339,7 +341,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...)
}
result, err := s.client.Search(ctx, searchRequest)
result, err := s.client(ctx).Search(ctx, searchRequest)
if err != nil {
errhttp.Write(ctx, err, w)
return

View File

@ -23,6 +23,7 @@ import (
func TestSearchFallback(t *testing.T) {
t.Run("should hit legacy search handler on mode 0", func(t *testing.T) {
mockClient := &MockClient{}
mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient }
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
@ -30,7 +31,8 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
@ -49,6 +51,7 @@ func TestSearchFallback(t *testing.T) {
t.Run("should hit legacy search handler on mode 1", func(t *testing.T) {
mockClient := &MockClient{}
mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient }
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
@ -56,7 +59,8 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
@ -75,6 +79,7 @@ func TestSearchFallback(t *testing.T) {
t.Run("should hit legacy search handler on mode 2", func(t *testing.T) {
mockClient := &MockClient{}
mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient }
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
@ -82,7 +87,8 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
@ -101,6 +107,7 @@ func TestSearchFallback(t *testing.T) {
t.Run("should hit unified storage search handler on mode 3", func(t *testing.T) {
mockClient := &MockClient{}
mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient }
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
@ -108,7 +115,8 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
@ -127,6 +135,7 @@ func TestSearchFallback(t *testing.T) {
t.Run("should hit unified storage search handler on mode 4", func(t *testing.T) {
mockClient := &MockClient{}
mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient }
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
@ -134,7 +143,8 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
@ -153,6 +163,7 @@ func TestSearchFallback(t *testing.T) {
t.Run("should hit unified storage search handler on mode 5", func(t *testing.T) {
mockClient := &MockClient{}
mockUnifiedCtxclient := func(context.Context) resource.ResourceClient { return mockClient }
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
@ -160,7 +171,8 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler := NewSearchHandler(tracing.NewNoopTracerService(), cfg, mockLegacyClient)
searchHandler.client = resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, mockUnifiedCtxclient, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
@ -185,7 +197,7 @@ func TestSearchHandler(t *testing.T) {
// Initialize the search handler with the mock client
searchHandler := SearchHandler{
log: log.New("test", "test"),
client: mockClient,
client: func(context.Context) resource.ResourceIndexClient { return mockClient },
tracer: tracing.NewNoopTracerService(),
}
@ -271,6 +283,7 @@ func TestSearchHandler(t *testing.T) {
// MockClient implements the ResourceIndexClient interface for testing
type MockClient struct {
resource.ResourceIndexClient
resource.ResourceIndex
// Capture the last SearchRequest for assertions
LastSearchRequest *resource.ResourceSearchRequest
@ -330,7 +343,45 @@ func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequ
},
}, nil
}
func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
return nil, nil
}
func (m *MockClient) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) {
return nil, nil
}
func (m *MockClient) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) {
return nil, nil
}
func (m *MockClient) Delete(ctx context.Context, in *resource.DeleteRequest, opts ...grpc.CallOption) (*resource.DeleteResponse, error) {
return nil, nil
}
func (m *MockClient) Create(ctx context.Context, in *resource.CreateRequest, opts ...grpc.CallOption) (*resource.CreateResponse, error) {
return nil, nil
}
func (m *MockClient) Update(ctx context.Context, in *resource.UpdateRequest, opts ...grpc.CallOption) (*resource.UpdateResponse, error) {
return nil, nil
}
func (m *MockClient) Read(ctx context.Context, in *resource.ReadRequest, opts ...grpc.CallOption) (*resource.ReadResponse, error) {
return nil, nil
}
func (m *MockClient) Restore(ctx context.Context, in *resource.RestoreRequest, opts ...grpc.CallOption) (*resource.RestoreResponse, error) {
return nil, nil
}
func (m *MockClient) GetBlob(ctx context.Context, in *resource.GetBlobRequest, opts ...grpc.CallOption) (*resource.GetBlobResponse, error) {
return nil, nil
}
func (m *MockClient) PutBlob(ctx context.Context, in *resource.PutBlobRequest, opts ...grpc.CallOption) (*resource.PutBlobResponse, error) {
return nil, nil
}
func (m *MockClient) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) {
return nil, nil
}
func (m *MockClient) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) {
return nil, nil
}
func (m *MockClient) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) {
return nil, nil
}
func (m *MockClient) BatchProcess(ctx context.Context, opts ...grpc.CallOption) (resource.BatchStore_BatchProcessClient, error) {
return nil, nil
}

View File

@ -82,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
features: features,
accessControl: accessControl,
unified: unified,
search: dashboard.NewSearchHandler(unified, tracing, cfg, legacyDashboardSearcher),
search: dashboard.NewSearchHandler(tracing, cfg, legacyDashboardSearcher),
legacy: &dashboard.DashboardStorage{
Resource: dashboardv0alpha1.DashboardResourceInfo,

View File

@ -16,6 +16,7 @@ import (
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/testutil"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardsservice "github.com/grafana/grafana/pkg/services/dashboards/service"
@ -51,7 +52,7 @@ func TestIntegrationAuthorize(t *testing.T) {
fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil)
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil)
require.NoError(t, err)
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())

View File

@ -19,6 +19,7 @@ import (
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/testutil"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardsservice "github.com/grafana/grafana/pkg/services/dashboards/service"
@ -63,7 +64,7 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil)
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil)
require.NoError(t, err)
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
repo := ProvideService(sql, cfg, features, tagService, tracing.InitializeTracerForTest(), ruleStore, dashSvc)
@ -246,7 +247,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) {
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
nil, sql, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil)
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil)
require.NoError(t, err)
dashSvc.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService())
cfg.AnnotationMaximumTagsLength = 60

View File

@ -12,15 +12,16 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"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/infra/log"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"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/storage/unified/resource"
k8sUser "k8s.io/apiserver/pkg/authentication/user"
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
@ -42,22 +43,25 @@ type K8sHandler interface {
var _ K8sHandler = (*k8sHandler)(nil)
type k8sHandler struct {
namespacer request.NamespaceMapper
gvr schema.GroupVersionResource
restConfigProvider apiserver.RestConfigProvider
searcher resource.ResourceIndexClient
userService user.Service
namespacer request.NamespaceMapper
gvr schema.GroupVersionResource
restConfig func(context.Context) *rest.Config
searcher func(context.Context) resource.ResourceIndexClient
userService user.Service
}
func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store, userSvc user.Service) K8sHandler {
func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource,
restConfig func(context.Context) *rest.Config, dashStore dashboards.Store, userSvc user.Service) K8sHandler {
legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore)
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher)
key := gvr.Resource + "." + gvr.Group // the unified storage key in the config.ini is resource + group
searchClient := resource.NewSearchClient(cfg, key, unified.GetResourceClient, legacySearcher)
return &k8sHandler{
namespacer: namespacer,
gvr: gvr,
restConfigProvider: restConfigProvider,
searcher: searchClient,
userService: userSvc,
namespacer: namespacer,
gvr: gvr,
restConfig: restConfig,
searcher: searchClient,
userService: userSvc,
}
}
@ -187,12 +191,12 @@ func (h *k8sHandler) Search(ctx context.Context, orgID int64, in *resource.Resou
}
}
return h.searcher.Search(ctx, in)
return h.searcher(ctx).Search(ctx, in)
}
func (h *k8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.ResourceStatsResponse, error) {
// goes directly through grpc, so doesn't need the new context
return h.searcher.GetStats(ctx, &resource.ResourceStatsRequest{
return h.searcher(ctx).GetStats(ctx, &resource.ResourceStatsRequest{
Namespace: h.GetNamespace(orgID),
Kinds: []string{
h.gvr.Group + "/" + h.gvr.Resource,
@ -223,7 +227,7 @@ func (h *k8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*use
}
func (h *k8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
cfg := h.restConfigProvider.GetRestConfig(ctx)
cfg := h.restConfig(ctx)
if cfg == nil {
return nil, false
}

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/rest"
)
var _ K8sHandler = (*MockK8sHandler)(nil)
@ -88,3 +89,11 @@ func (m *MockK8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (
}
return args.Get(0).(*user.User), args.Error(1)
}
type MockTestRestConfig struct {
cfg *rest.Config
}
func (r MockTestRestConfig) GetRestConfig(ctx context.Context) *rest.Config {
return r.cfg
}

View File

@ -93,10 +93,10 @@ func ProvideDashboardServiceImpl(
cfg *setting.Cfg, dashboardStore dashboards.Store, folderStore folder.FolderStore,
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
ac accesscontrol.AccessControl, folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer,
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
restConfigProvider apiserver.RestConfigProvider, userService user.Service,
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
) (*DashboardServiceImpl, error) {
k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService)
k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), dashboardv0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService)
dashSvc := &DashboardServiceImpl{
cfg: cfg,

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
accesscontrolmock "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/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@ -904,8 +905,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
folderService,
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,
@ -993,8 +993,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
folderService,
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,
@ -1040,8 +1039,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
folderService,
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,
@ -1106,8 +1104,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
folderService,
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,
@ -1179,8 +1176,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
folderService,
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,

View File

@ -12,6 +12,7 @@ import (
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/db"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
@ -110,8 +111,7 @@ func TestValidateDashboardExists(t *testing.T) {
foldertest.NewFakeService(),
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotatest.New(false, nil),
nil,

View File

@ -52,8 +52,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash
cfg,
request.GetNamespaceMapper(cfg),
v0alpha1.DashboardResourceInfo.GroupVersionResource(),
restConfigProvider,
unified,
restConfigProvider.GetRestConfig,
dashboardStore,
userService,
),

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -25,6 +25,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"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
@ -355,8 +356,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
folderSvc,
folder.NewFakeStore(),
nil,
nil,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,
@ -453,7 +453,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena
cfg, dashboardStore, folderStore,
features, folderPermissions, ac,
folderSvc, fStore,
nil, nil, nil, nil, quotaService, nil, nil,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(t, svcErr)
dashboardService.RegisterDashboardPermissions(dashboardPermissions)
@ -526,7 +526,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg, dashboardStore, folderStore,
features, folderPermissions, ac,
folderSvc, fStore,
nil, nil, nil, nil, quotaService, nil, nil,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(t, dashSvcErr)
dashService.RegisterDashboardPermissions(dashboardPermissions)

View File

@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"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/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
@ -735,7 +736,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
cfg, dashboardStore, folderStore,
features, acmock.NewMockedPermissionsService(), ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil, nil, nil, nil, quotaService, nil, nil,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(t, err)
service.RegisterDashboardPermissions(dashPermissionService)
@ -833,7 +834,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg, dashStore, folderStore,
features, acmock.NewMockedPermissionsService(), ac,
folderSvc, folder.NewFakeStore(),
nil, nil, nil, nil, quotaService, nil, nil,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(t, err)
dashService.RegisterDashboardPermissions(dashPermissionService)

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
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/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
@ -62,7 +63,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
cfg, dashboardStore, fs,
features, folderPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil, nil, nil, nil, quotaService, nil, nil,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil,
)
require.NoError(tb, err)
dashboardService.RegisterDashboardPermissions(dashboardPermissions)

View File

@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/dashboards/service"
@ -325,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
dashService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStoreService, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), ac,
foldertest.NewFakeService(), folder.NewFakeStore(), nil, nil, nil, nil, quotatest.New(false, nil), nil, nil,
foldertest.NewFakeService(), folder.NewFakeStore(), nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil,
)
require.NoError(t, err)
dashService.RegisterDashboardPermissions(dashPermissionService)

View File

@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"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"
dashboardsDB "github.com/grafana/grafana/pkg/services/dashboards/database"
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
@ -1398,7 +1399,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) {
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
nil, testDB, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, nil, nil, nil, quotatest.New(false, nil), nil, nil)
dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil)
require.NoError(t, err)
dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{})
fakeGuardian := &guardian.FakeDashboardGuardian{

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authimpl"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -496,7 +497,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest())
dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(),
ac, folderSvc, fStore, nil, nil, nil, nil, quotaService, nil, nil)
ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil)
require.NoError(t, err)
dashService.RegisterDashboardPermissions(acmock.NewMockedPermissionsService())
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())

View File

@ -1,20 +1,22 @@
package resource
import (
"context"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/setting"
)
func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient ResourceIndexClient, legacyClient ResourceIndexClient) ResourceIndexClient {
func NewSearchClient(cfg *setting.Cfg, unifiedStorageConfigKey string, unifiedClient func(context.Context) ResourceClient, legacyClient ResourceIndexClient) func(context.Context) ResourceIndexClient {
config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey]
if !ok {
return legacyClient
return func(ctx context.Context) ResourceIndexClient { return legacyClient }
}
switch config.DualWriterMode {
case rest.Mode0, rest.Mode1, rest.Mode2:
return legacyClient
return func(ctx context.Context) ResourceIndexClient { return legacyClient }
default:
return unifiedClient
return func(ctx context.Context) ResourceIndexClient { return unifiedClient(ctx) }
}
}