mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Setup legacy search based on mode (#98908)
This commit is contained in:
parent
060182a3ba
commit
be8396cafa
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||||
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
|
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"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"
|
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
@ -54,8 +55,9 @@ type dashboardSqlAccess struct {
|
|||||||
provisioning provisioning.ProvisioningService
|
provisioning provisioning.ProvisioningService
|
||||||
|
|
||||||
// Use for writing (not reading)
|
// Use for writing (not reading)
|
||||||
dashStore dashboards.Store
|
dashStore dashboards.Store
|
||||||
softDelete bool
|
softDelete bool
|
||||||
|
dashboardSearchClient legacysearcher.DashboardSearchClient
|
||||||
|
|
||||||
// Typically one... the server wrapper
|
// Typically one... the server wrapper
|
||||||
subscribers []chan *resource.WrittenEvent
|
subscribers []chan *resource.WrittenEvent
|
||||||
@ -68,12 +70,14 @@ func NewDashboardAccess(sql legacysql.LegacyDatabaseProvider,
|
|||||||
provisioning provisioning.ProvisioningService,
|
provisioning provisioning.ProvisioningService,
|
||||||
softDelete bool,
|
softDelete bool,
|
||||||
) DashboardAccess {
|
) DashboardAccess {
|
||||||
|
dashboardSearchClient := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||||
return &dashboardSqlAccess{
|
return &dashboardSqlAccess{
|
||||||
sql: sql,
|
sql: sql,
|
||||||
namespacer: namespacer,
|
namespacer: namespacer,
|
||||||
dashStore: dashStore,
|
dashStore: dashStore,
|
||||||
provisioning: provisioning,
|
provisioning: provisioning,
|
||||||
softDelete: softDelete,
|
softDelete: softDelete,
|
||||||
|
dashboardSearchClient: *dashboardSearchClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
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
|
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) {
|
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) {
|
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.
|
// GetStats implements ResourceServer.
|
||||||
func (a *dashboardSqlAccess) GetStats(ctx context.Context, req *resource.ResourceStatsRequest) (*resource.ResourceStatsResponse, error) {
|
func (a *dashboardSqlAccess) GetStats(ctx context.Context, req *resource.ResourceStatsRequest) (*resource.ResourceStatsResponse, error) {
|
||||||
info, err := claims.ParseNamespace(req.Namespace)
|
return a.dashboardSearchClient.GetStats(ctx, req)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
128
pkg/registry/apis/dashboard/legacysearcher/search_client.go
Normal file
128
pkg/registry/apis/dashboard/legacysearcher/search_client.go
Normal 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
|
||||||
|
}
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||||
dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search"
|
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/storage/unified/resource"
|
||||||
"github.com/grafana/grafana/pkg/util/errhttp"
|
"github.com/grafana/grafana/pkg/util/errhttp"
|
||||||
)
|
)
|
||||||
@ -32,9 +33,10 @@ type SearchHandler struct {
|
|||||||
tracer trace.Tracer
|
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{
|
return &SearchHandler{
|
||||||
client: client,
|
client: searchClient,
|
||||||
log: log.New("grafana-apiserver.dashboards.search"),
|
log: log.New("grafana-apiserver.dashboards.search"),
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
}
|
}
|
||||||
@ -332,7 +334,6 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
|
|||||||
searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...)
|
searchRequest.Options.Fields = append(searchRequest.Options.Fields, namesFilter...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the query
|
|
||||||
result, err := s.client.Search(ctx, searchRequest)
|
result, err := s.client.Search(ctx, searchRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errhttp.Write(ctx, err, w)
|
errhttp.Write(ctx, err, w)
|
||||||
|
@ -7,13 +7,173 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"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/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
"google.golang.org/grpc"
|
"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) {
|
func TestSearchHandlerFields(t *testing.T) {
|
||||||
// Create a mock client
|
// Create a mock client
|
||||||
mockClient := &MockClient{}
|
mockClient := &MockClient{}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard"
|
"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/legacy"
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"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)
|
softDelete := features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore)
|
||||||
dbp := legacysql.NewDatabaseProvider(sql)
|
dbp := legacysql.NewDatabaseProvider(sql)
|
||||||
namespacer := request.GetNamespaceMapper(cfg)
|
namespacer := request.GetNamespaceMapper(cfg)
|
||||||
|
legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore)
|
||||||
builder := &DashboardsAPIBuilder{
|
builder := &DashboardsAPIBuilder{
|
||||||
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
|
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
|
||||||
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
|
||||||
@ -80,7 +82,7 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
|
|||||||
features: features,
|
features: features,
|
||||||
accessControl: accessControl,
|
accessControl: accessControl,
|
||||||
unified: unified,
|
unified: unified,
|
||||||
search: dashboard.NewSearchHandler(unified, tracing),
|
search: dashboard.NewSearchHandler(unified, tracing, cfg, legacyDashboardSearcher),
|
||||||
|
|
||||||
legacy: &dashboard.DashboardStorage{
|
legacy: &dashboard.DashboardStorage{
|
||||||
Resource: dashboardv0alpha1.DashboardResourceInfo,
|
Resource: dashboardv0alpha1.DashboardResourceInfo,
|
||||||
|
@ -12,8 +12,11 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"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"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"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"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
k8sUser "k8s.io/apiserver/pkg/authentication/user"
|
k8sUser "k8s.io/apiserver/pkg/authentication/user"
|
||||||
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
|
k8sRequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
@ -40,12 +43,14 @@ type k8sHandler struct {
|
|||||||
searcher resource.ResourceIndexClient
|
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{
|
return &k8sHandler{
|
||||||
namespacer: namespacer,
|
namespacer: namespacer,
|
||||||
gvr: gvr,
|
gvr: gvr,
|
||||||
restConfigProvider: restConfigProvider,
|
restConfigProvider: restConfigProvider,
|
||||||
searcher: searcher,
|
searcher: searchClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ func ProvideDashboardServiceImpl(
|
|||||||
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
|
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
|
||||||
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
|
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
|
||||||
) (*DashboardServiceImpl, error) {
|
) (*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{
|
dashSvc := &DashboardServiceImpl{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
@ -540,6 +540,8 @@ type Cfg struct {
|
|||||||
HttpsSkipVerify bool
|
HttpsSkipVerify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UnifiedStorageConfigKeyDashboard = "dashboards.dashboard.grafana.app"
|
||||||
|
|
||||||
type UnifiedStorageConfig struct {
|
type UnifiedStorageConfig struct {
|
||||||
DualWriterMode rest.DualWriterMode
|
DualWriterMode rest.DualWriterMode
|
||||||
DualWriterPeriodicDataSyncJobEnabled bool
|
DualWriterPeriodicDataSyncJobEnabled bool
|
||||||
|
@ -17,6 +17,7 @@ require (
|
|||||||
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.263.0
|
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/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/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||||
github.com/prometheus/client_golang v1.20.5
|
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-app-sdk/logging v0.29.0 // indirect
|
||||||
github.com/grafana/grafana-aws-sdk v0.31.5 // 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-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/otel-profiling-go v0.5.1 // indirect
|
||||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
||||||
github.com/grafana/sqlds/v4 v4.1.3 // indirect
|
github.com/grafana/sqlds/v4 v4.1.3 // indirect
|
||||||
|
20
pkg/storage/unified/resource/search_client.go
Normal file
20
pkg/storage/unified/resource/search_client.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user