Setup legacy search based on mode (#98908)

This commit is contained in:
William Assis 2025-01-27 15:32:07 -03:00 committed by GitHub
parent 060182a3ba
commit be8396cafa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 339 additions and 49 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
@ -54,8 +55,9 @@ type dashboardSqlAccess struct {
provisioning provisioning.ProvisioningService
// Use for writing (not reading)
dashStore dashboards.Store
softDelete bool
dashStore dashboards.Store
softDelete bool
dashboardSearchClient legacysearcher.DashboardSearchClient
// Typically one... the server wrapper
subscribers []chan *resource.WrittenEvent
@ -68,12 +70,14 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
provisioning provisioning.ProvisioningService,
softDelete bool,
) DashboardAccess {
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore)
return &dashboardSqlAccess{
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
softDelete: softDelete,
sql: sql,
namespacer: namespacer,
dashStore: dashStore,
provisioning: provisioning,
softDelete: softDelete,
dashboardSearchClient: *dashboardSearchClient,
}
}

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
claims "github.com/grafana/authlib/types"
@ -255,9 +254,8 @@ func (a *dashboardSqlAccess) Read(ctx context.Context, req *resource.ReadRequest
return a.ReadResource(ctx, req), nil
}
// TODO: this needs to be implemented
func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.ResourceSearchRequest) (*resource.ResourceSearchResponse, error) {
return nil, fmt.Errorf("not yet (filter)")
return a.dashboardSearchClient.Search(ctx, req)
}
func (a *dashboardSqlAccess) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
@ -270,35 +268,5 @@ func (a *dashboardSqlAccess) CountRepositoryObjects(context.Context, *resource.C
// GetStats implements ResourceServer.
func (a *dashboardSqlAccess) GetStats(ctx context.Context, req *resource.ResourceStatsRequest) (*resource.ResourceStatsResponse, error) {
info, err := claims.ParseNamespace(req.Namespace)
if err != nil {
return nil, fmt.Errorf("unable to read namespace")
}
if info.OrgID == 0 {
return nil, fmt.Errorf("invalid OrgID found in namespace")
}
if len(req.Kinds) != 1 {
return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback")
}
parts := strings.SplitN(req.Kinds[0], "/", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid kind")
}
count, err := a.dashStore.CountInOrg(ctx, info.OrgID)
if err != nil {
return nil, err
}
return &resource.ResourceStatsResponse{
Stats: []*resource.ResourceStatsResponse_Stats{
{
Group: parts[0],
Resource: parts[1],
Count: count,
},
},
}, nil
return a.dashboardSearchClient.GetStats(ctx, req)
}

View File

@ -0,0 +1,128 @@
package legacysearcher
import (
"context"
"fmt"
"strings"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"google.golang.org/grpc"
)
type DashboardSearchClient struct {
resource.ResourceIndexClient
dashboardStore dashboards.Store
}
func NewDashboardSearchClient(dashboardStore dashboards.Store) *DashboardSearchClient {
return &DashboardSearchClient{dashboardStore: dashboardStore}
}
func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
if req.Query == "*" {
req.Query = ""
}
// TODO add missing support for the following query params:
// - tag
// - starred (won't support)
// - page (check)
// - type
// - sort
// - deleted
// - permission
// - dashboardIds
// - dashboardUIDs
// - folderIds
// - folderUIDs
// - sort (default by title)
query := &dashboards.FindPersistedDashboardsQuery{
Title: req.Query,
Limit: req.Limit,
// FolderUIDs: req.FolderUIDs,
SignedInUser: user,
}
// TODO need to test this
// emptyResponse, err := a.dashService.GetSharedDashboardUIDsQuery(ctx, query)
// if err != nil {
// return nil, err
// } else if emptyResponse {
// return nil, nil
// }
res, err := c.dashboardStore.FindDashboards(ctx, query)
if err != nil {
return nil, err
}
// TODO sort if query.Sort == "" see sortedHits in services/search/service.go
searchFields := resource.StandardSearchFields()
list := &resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
// searchFields.Field(resource.SEARCH_FIELD_TAGS),
},
},
}
for _, dashboard := range res {
list.Results.Rows = append(list.Results.Rows, &resource.ResourceTableRow{
Key: &resource.ResourceKey{
Namespace: "default",
Group: "dashboard.grafana.app",
Resource: "dashboards",
Name: dashboard.UID,
},
Cells: [][]byte{[]byte(dashboard.Title), []byte(dashboard.FolderUID)}, // TODO add tag
})
}
return list, nil
}
func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
info, err := claims.ParseNamespace(req.Namespace)
if err != nil {
return nil, fmt.Errorf("unable to read namespace")
}
if info.OrgID == 0 {
return nil, fmt.Errorf("invalid OrgID found in namespace")
}
if len(req.Kinds) != 1 {
return nil, fmt.Errorf("only can query for dashboard kind in legacy fallback")
}
parts := strings.SplitN(req.Kinds[0], "/", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid kind")
}
count, err := c.dashboardStore.CountInOrg(ctx, info.OrgID)
if err != nil {
return nil, err
}
return &resource.ResourceStatsResponse{
Stats: []*resource.ResourceStatsResponse_Stats{
{
Group: parts[0],
Resource: parts[1],
Count: count,
},
},
}, nil
}

View File

@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/util/errhttp"
)
@ -32,9 +33,10 @@ type SearchHandler struct {
tracer trace.Tracer
}
func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer) *SearchHandler {
func NewSearchHandler(client resource.ResourceIndexClient, tracer trace.Tracer, cfg *setting.Cfg, legacyDashboardSearcher resource.ResourceIndexClient) *SearchHandler {
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, client, legacyDashboardSearcher)
return &SearchHandler{
client: client,
client: searchClient,
log: log.New("grafana-apiserver.dashboards.search"),
tracer: tracer,
}
@ -332,7 +334,6 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...)
}
// Run the query
result, err := s.client.Search(ctx, searchRequest)
if err != nil {
errhttp.Write(ctx, err, w)

View File

@ -7,13 +7,173 @@ import (
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"google.golang.org/grpc"
)
func TestSearchFallback(t *testing.T) {
t.Run("should hit legacy search handler on mode 0", func(t *testing.T) {
mockClient := &MockClient{}
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode0},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
if mockClient.LastSearchRequest != nil {
t.Fatalf("expected Search NOT to be called, but it was")
}
if mockLegacyClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
})
t.Run("should hit legacy search handler on mode 1", func(t *testing.T) {
mockClient := &MockClient{}
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode1},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
if mockClient.LastSearchRequest != nil {
t.Fatalf("expected Search NOT to be called, but it was")
}
if mockLegacyClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
})
t.Run("should hit legacy search handler on mode 2", func(t *testing.T) {
mockClient := &MockClient{}
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode2},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
if mockClient.LastSearchRequest != nil {
t.Fatalf("expected Search NOT to be called, but it was")
}
if mockLegacyClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
})
t.Run("should hit unified storage search handler on mode 3", func(t *testing.T) {
mockClient := &MockClient{}
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode3},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
if mockClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
if mockLegacyClient.LastSearchRequest != nil {
t.Fatalf("expected Search NOT to be called, but it was")
}
})
t.Run("should hit unified storage search handler on mode 4", func(t *testing.T) {
mockClient := &MockClient{}
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode4},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
if mockClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
if mockLegacyClient.LastSearchRequest != nil {
t.Fatalf("expected Search NOT to be called, but it was")
}
})
t.Run("should hit unified storage search handler on mode 5", func(t *testing.T) {
mockClient := &MockClient{}
mockLegacyClient := &MockClient{}
cfg := &setting.Cfg{
UnifiedStorage: map[string]setting.UnifiedStorageConfig{
"dashboards.dashboard.grafana.app": {DualWriterMode: rest.Mode5},
},
}
searchHandler := NewSearchHandler(mockClient, tracing.NewNoopTracerService(), cfg, mockLegacyClient)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/search", nil)
req.Header.Add("content-type", "application/json")
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test"}))
searchHandler.DoSearch(rr, req)
if mockClient.LastSearchRequest == nil {
t.Fatalf("expected Search to be called, but it was not")
}
if mockLegacyClient.LastSearchRequest != nil {
t.Fatalf("expected Search NOT to be called, but it was")
}
})
}
func TestSearchHandlerFields(t *testing.T) {
// Create a mock client
mockClient := &MockClient{}

View File

@ -24,6 +24,7 @@ import (
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
@ -71,6 +72,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
dbp := legacysql.NewDatabaseProvider(sql)
namespacer := request.GetNamespaceMapper(cfg)
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
@ -80,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
features: features,
accessControl: accessControl,
unified: unified,
search: dashboard.NewSearchHandler(unified, tracing),
search: dashboard.NewSearchHandler(unified, tracing, cfg, legacyDashboardSearcher),
legacy: &dashboard.DashboardStorage{
Resource: dashboardv0alpha1.DashboardResourceInfo,

View File

@ -12,8 +12,11 @@ import (
"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/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
k8sUser "k8s.io/apiserver/pkg/authentication/user"
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
@ -40,12 +43,14 @@ type k8sHandler struct {
searcher resource.ResourceIndexClient
}
func NewK8sHandler(namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient) K8sHandler {
func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store) K8sHandler {
legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore)
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher)
return &k8sHandler{
namespacer: namespacer,
gvr: gvr,
restConfigProvider: restConfigProvider,
searcher: searcher,
searcher: searchClient,
}
}

View File

@ -91,7 +91,7 @@ func ProvideDashboardServiceImpl(
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
) (*DashboardServiceImpl, error) {
k8sHandler := client.NewK8sHandler(request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified)
k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore)
dashSvc := &DashboardServiceImpl{
cfg: cfg,

View File

@ -540,6 +540,8 @@ type Cfg struct {
HttpsSkipVerify bool
}
const UnifiedStorageConfigKeyDashboard = "dashboards.dashboard.grafana.app"
type UnifiedStorageConfig struct {
DualWriterMode rest.DualWriterMode
DualWriterPeriodicDataSyncJobEnabled bool

View File

@ -17,6 +17,7 @@ require (
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
github.com/grafana/grafana-plugin-sdk-go v0.263.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/prometheus/client_golang v1.20.5
@ -120,7 +121,6 @@ require (
github.com/grafana/grafana-app-sdk/logging v0.29.0 // indirect
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect
github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d // indirect
github.com/grafana/otel-profiling-go v0.5.1 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
github.com/grafana/sqlds/v4 v4.1.3 // indirect

View File

@ -0,0 +1,20 @@
package resource
import (
"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 {
config, ok := cfg.UnifiedStorage[unifiedStorageConfigKey]
if !ok {
return legacyClient
}
switch config.DualWriterMode {
case rest.Mode0, rest.Mode1, rest.Mode2:
return legacyClient
default:
return unifiedClient
}
}