diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index a08deefb4bb..36a329e5faa 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -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) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index d6645c1b945..42fc5d52d55 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -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) diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 824243692a1..c4ad9532066 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -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 diff --git a/pkg/registry/apis/dashboard/search_test.go b/pkg/registry/apis/dashboard/search_test.go index d48a6e7a802..91af78295a3 100644 --- a/pkg/registry/apis/dashboard/search_test.go +++ b/pkg/registry/apis/dashboard/search_test.go @@ -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 +} diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go index c75b159961f..c051bb7595f 100644 --- a/pkg/registry/apis/dashboard/v0alpha1/register.go +++ b/pkg/registry/apis/dashboard/v0alpha1/register.go @@ -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, diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 2e982273e28..4070d1d6b5f 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -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()) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 555ebbe4e88..0ac44989ec8 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -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 diff --git a/pkg/services/apiserver/client/client.go b/pkg/services/apiserver/client/client.go index 9aaf573b7c8..efeee1c573d 100644 --- a/pkg/services/apiserver/client/client.go +++ b/pkg/services/apiserver/client/client.go @@ -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 } diff --git a/pkg/services/apiserver/client/client_mock.go b/pkg/services/apiserver/client/client_mock.go index b5e2fc2eecb..57b6c4183f8 100644 --- a/pkg/services/apiserver/client/client_mock.go +++ b/pkg/services/apiserver/client/client_mock.go @@ -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 +} diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 9508f9711e6..3964922fcda 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -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, diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index b2deeba68fe..470ba022383 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -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, diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 48db9813f85..b125e159ed4 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -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, diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index 98406b6a7b4..8b83d379f5a 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -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, ), diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index 9ac7c8a2e1e..0b4dc311fa4 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -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 } diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index 988fd4c5d95..87b8787f8fb 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -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, diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 203aa4f37ab..94a19aa17b5 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -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) -} diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 4421b441113..8a7e99a991c 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -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) }) } diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 481ab6c704d..c46f9ada849 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -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)) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 7ea77dd22ff..8fddbc1e1f2 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -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) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index f9e9c8c8d75..75980513f1b 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -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) diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index 7aa8c1d45b3..b7175b5dca6 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -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) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index d0a87802ab8..e81d34c6d37 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -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) diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index bec987a72a8..6605c391488 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -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{ diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 5cf4a3acabf..5f70f869e3a 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -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()) diff --git a/pkg/storage/unified/resource/search_client.go b/pkg/storage/unified/resource/search_client.go index 43e0faa7b1e..8615a8cbed2 100644 --- a/pkg/storage/unified/resource/search_client.go +++ b/pkg/storage/unified/resource/search_client.go @@ -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) } } }