Files

524 lines
16 KiB
Go

package legacysearcher
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"google.golang.org/grpc"
"k8s.io/apimachinery/pkg/selection"
claims "github.com/grafana/authlib/types"
dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
unisearch "github.com/grafana/grafana/pkg/storage/unified/search"
)
type DashboardSearchClient struct {
resourcepb.ResourceIndexClient
dashboardStore dashboards.Store
sorter sort.Service
}
func NewDashboardSearchClient(dashboardStore dashboards.Store, sorter sort.Service) *DashboardSearchClient {
return &DashboardSearchClient{dashboardStore: dashboardStore, sorter: sorter}
}
var sortByMapping = map[string]string{
unisearch.DASHBOARD_VIEWS_LAST_30_DAYS: "viewed-recently",
unisearch.DASHBOARD_VIEWS_TOTAL: "viewed",
unisearch.DASHBOARD_ERRORS_LAST_30_DAYS: "errors-recently",
unisearch.DASHBOARD_ERRORS_TOTAL: "errors",
"title": "alpha",
}
func ParseSortName(sortName string) (string, bool, error) {
if sortName == "" {
return "", false, nil
}
isDesc := strings.HasSuffix(sortName, "-desc")
isAsc := strings.HasSuffix(sortName, "-asc")
// default to desc if no suffix is provided
if !isDesc && !isAsc {
isDesc = true
}
prefix := strings.TrimSuffix(strings.TrimSuffix(sortName, "-desc"), "-asc")
for key, mappedPrefix := range sortByMapping {
if prefix == mappedPrefix {
return key, isDesc, nil
}
}
return "", false, fmt.Errorf("no matching sort field found for: %s", sortName)
}
// nolint:gocyclo
func (c *DashboardSearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
// the "*"s will be added in the k8s handler in dashboard_service.go in order to make search work
// in modes 3+. These "*"s will break the legacy sql query so we need to remove them here
if strings.Contains(req.Query, "*") {
req.Query = strings.ReplaceAll(req.Query, "*", "")
}
query := &dashboards.FindPersistedDashboardsQuery{
Title: req.Query,
Limit: req.Limit,
Page: req.Page,
SignedInUser: user,
IsDeleted: req.IsDeleted,
}
if req.Permission == int64(dashboardaccess.PERMISSION_EDIT) {
query.Permission = dashboardaccess.PERMISSION_EDIT
}
var queryType string
switch req.Options.Key.Resource {
case dashboard.DASHBOARD_RESOURCE:
queryType = searchstore.TypeDashboard
case folders.RESOURCE:
queryType = searchstore.TypeFolder
default:
return nil, fmt.Errorf("bad type request")
}
if len(req.Federated) > 1 {
return nil, fmt.Errorf("bad type request")
}
if len(req.Federated) == 1 &&
((req.Federated[0].Resource == dashboard.DASHBOARD_RESOURCE && queryType == searchstore.TypeFolder) ||
(req.Federated[0].Resource == folders.RESOURCE && queryType == searchstore.TypeDashboard)) {
queryType = "" // makes the legacy store search across both
}
if queryType != "" {
query.Type = queryType
}
sortByField := ""
if len(req.SortBy) != 0 {
if len(req.SortBy) > 1 {
return nil, fmt.Errorf("only one sort field is supported")
}
sort := req.SortBy[0]
sortByField = strings.TrimPrefix(sort.Field, resource.SEARCH_FIELD_PREFIX)
sorterName := sortByMapping[sortByField]
if sort.Desc {
sorterName += "-desc"
} else {
sorterName += "-asc"
}
if sorter, ok := c.sorter.GetSortOption(sorterName); ok {
query.Sort = sorter
}
}
// the title search will not return any sortMeta (an int64), like
// most sorting will. Without this, the title will be set to sortMeta (0)
if sortByField == resource.SEARCH_FIELD_TITLE {
sortByField = ""
}
// if searching for tags, get those instead of the dashboards or folders
for facet := range req.Facet {
if facet == resource.SEARCH_FIELD_TAGS {
tags, err := c.dashboardStore.GetDashboardTags(ctx, &dashboards.GetDashboardTagsQuery{
OrgID: user.GetOrgID(),
})
if err != nil {
return nil, err
}
list := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{},
Facet: map[string]*resourcepb.ResourceSearchResponse_Facet{
"tags": {
Terms: []*resourcepb.ResourceSearchResponse_TermFacet{},
},
},
}
for _, tag := range tags {
list.Facet["tags"].Terms = append(list.Facet["tags"].Terms, &resourcepb.ResourceSearchResponse_TermFacet{
Term: tag.Term,
Count: int64(tag.Count),
})
}
return list, nil
}
}
// handle deprecated dashboardIds query param
for _, field := range req.Options.Labels {
if field.Key == utils.LabelKeyDeprecatedInternalID {
values := field.GetValues()
dashboardIds := make([]int64, len(values))
for i, id := range values {
if n, err := strconv.ParseInt(id, 10, 64); err == nil {
dashboardIds[i] = n
}
}
query.DashboardIds = dashboardIds
}
}
for _, field := range req.Options.Fields {
vals := field.GetValues()
switch field.Key {
case resource.SEARCH_FIELD_TAGS:
query.Tags = field.GetValues()
case resource.SEARCH_FIELD_NAME:
query.DashboardUIDs = field.GetValues()
query.DashboardIds = nil
case resource.SEARCH_FIELD_FOLDER:
folders := make([]string, len(vals))
for i, val := range vals {
if val == "" {
folders[i] = "general"
} else {
folders[i] = val
}
}
query.FolderUIDs = folders
case resource.SEARCH_FIELD_SOURCE_PATH:
// only one value is supported in legacy search
if len(vals) != 1 {
return nil, fmt.Errorf("only one repo path query is supported")
}
query.SourcePath = vals[0]
case resource.SEARCH_FIELD_MANAGER_KIND:
if len(vals) != 1 {
return nil, fmt.Errorf("only one manager kind supported")
}
query.ManagedBy = utils.ManagerKind(vals[0])
case resource.SEARCH_FIELD_MANAGER_ID:
if field.Operator == string(selection.NotIn) {
query.ManagerIdentityNotIn = vals
continue
}
// only one value is supported in legacy search
if len(vals) != 1 {
return nil, fmt.Errorf("only one repo name is supported")
}
query.ManagerIdentity = vals[0]
case unisearch.DASHBOARD_LIBRARY_PANEL_REFERENCE:
if len(vals) != 1 {
return nil, fmt.Errorf("only one library panel uid is supported")
}
return c.getLibraryPanelConnections(ctx, user, vals[0], req.Options.Key.Namespace)
case resource.SEARCH_FIELD_TITLE_PHRASE:
if len(vals) != 1 {
return nil, fmt.Errorf("only one title supported")
}
query.Title = vals[0]
query.TitleExactMatch = true
}
}
columns := c.getColumns(sortByField, query)
list := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: columns,
},
}
// if we are querying for provisioning information, we need to use a different
// legacy sql query, since legacy search does not support this
if query.ManagerIdentity != "" || len(query.ManagerIdentityNotIn) > 0 || query.ManagedBy != "" {
if query.ManagedBy == utils.ManagerKindUnknown {
return nil, fmt.Errorf("query by manager identity also requires manager.kind parameter")
}
// for plugin and orphaned dashboards, we will only return the manager kind alongside the regular search response
if query.ManagedBy == utils.ManagerKindPlugin || len(query.ManagerIdentityNotIn) > 0 {
var dashes []*dashboards.Dashboard
if query.ManagedBy == utils.ManagerKindPlugin {
dashes, err = c.dashboardStore.GetDashboardsByPluginID(ctx, &dashboards.GetDashboardsByPluginIDQuery{
PluginID: query.ManagerIdentity,
OrgID: user.GetOrgID(),
})
} else {
dashes, err = c.dashboardStore.GetOrphanedProvisionedDashboards(ctx, query.ManagerIdentityNotIn, user.GetOrgID())
}
if err != nil {
return nil, err
}
for _, dashboard := range dashes {
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(&dashboards.DashboardSearchProjection{
UID: dashboard.UID,
}, req.Options.Key.Namespace),
Cells: c.createProvisioningCells(dashboard, query),
})
}
list.TotalHits = int64(len(list.Results.Rows))
return list, nil
}
// for classic FP, we will return the regular search response alongside all the data in the dashboard_provisioning table
provisioningData := []*dashboards.DashboardProvisioningSearchResults{}
if query.ManagerIdentity == "" {
var data *dashboards.DashboardProvisioningSearchResults
if len(query.DashboardIds) > 0 {
data, err = c.dashboardStore.GetProvisionedDataByDashboardID(ctx, query.DashboardIds[0])
} else if len(query.DashboardUIDs) > 0 {
data, err = c.dashboardStore.GetProvisionedDataByDashboardUID(ctx, user.GetOrgID(), query.DashboardUIDs[0])
}
if err != nil {
return nil, err
}
if data != nil {
provisioningData = append(provisioningData, data)
}
} else {
provisioningData, err = c.dashboardStore.GetProvisionedDashboardsByName(ctx, query.ManagerIdentity, user.GetOrgID())
if err != nil {
return nil, err
}
}
for _, dashboard := range provisioningData {
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(&dashboards.DashboardSearchProjection{
UID: dashboard.Dashboard.UID,
}, req.Options.Key.Namespace),
Cells: c.createDetailedProvisioningCells(dashboard, query),
})
}
list.TotalHits = int64(len(list.Results.Rows))
return list, nil
}
res, err := c.dashboardStore.FindDashboards(ctx, query)
if err != nil {
return nil, err
}
for _, dashboard := range res {
cells, err := c.createBaseCells(dashboard, sortByField)
if err != nil {
return nil, err
}
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(&dashboard, req.Options.Key.Namespace),
Cells: cells,
})
}
list.TotalHits = int64(len(list.Results.Rows))
return list, nil
}
func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string) *resourcepb.ResourceKey {
if item.IsFolder {
return &resourcepb.ResourceKey{
Namespace: namespace,
Group: folders.GROUP,
Resource: folders.RESOURCE,
Name: item.UID,
}
}
return &resourcepb.ResourceKey{
Namespace: namespace,
Group: dashboard.GROUP,
Resource: dashboard.DASHBOARD_RESOURCE,
Name: item.UID,
}
}
// retrieves all the dashboards that are connected to the given library panel
func (c *DashboardSearchClient) getLibraryPanelConnections(ctx context.Context, user identity.Requester, libraryElementUID, namespace string) (*resourcepb.ResourceSearchResponse, error) {
connections, err := c.dashboardStore.GetDashboardsByLibraryPanelUID(ctx, libraryElementUID, user.GetOrgID())
if err != nil {
return nil, err
}
columns := c.getColumns("", &dashboards.FindPersistedDashboardsQuery{})
list := &resourcepb.ResourceSearchResponse{
Results: &resourcepb.ResourceTable{
Columns: columns,
},
}
for _, dashboard := range connections {
cells := c.createCommonCells("", dashboard.FolderUID, dashboard.ID, nil) // nolint:staticcheck
list.Results.Rows = append(list.Results.Rows, &resourcepb.ResourceTableRow{
Key: getResourceKey(&dashboards.DashboardSearchProjection{
UID: dashboard.UID,
}, namespace),
Cells: cells,
})
}
list.TotalHits = int64(len(list.Results.Rows))
return list, nil
}
func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resourcepb.ResourceStatsRequest, _ ...grpc.CallOption) (*resourcepb.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")
}
var count int64
switch parts[0] {
case dashboard.GROUP:
count, err = c.dashboardStore.CountInOrg(ctx, info.OrgID, false)
case folders.GROUP:
count, err = c.dashboardStore.CountInOrg(ctx, info.OrgID, true)
default:
return nil, fmt.Errorf("invalid group")
}
if err != nil {
return nil, err
}
return &resourcepb.ResourceStatsResponse{
Stats: []*resourcepb.ResourceStatsResponse_Stats{
{
Group: parts[0],
Resource: parts[1],
Count: count,
},
},
}, nil
}
func (c *DashboardSearchClient) getColumns(sortByField string, query *dashboards.FindPersistedDashboardsQuery) []*resourcepb.ResourceTableColumnDefinition {
searchFields := resource.StandardSearchFields()
columns := []*resourcepb.ResourceTableColumnDefinition{
searchFields.Field(resource.SEARCH_FIELD_TITLE),
searchFields.Field(resource.SEARCH_FIELD_FOLDER),
searchFields.Field(resource.SEARCH_FIELD_TAGS),
searchFields.Field(resource.SEARCH_FIELD_LEGACY_ID),
}
if query.ManagerIdentity != "" || len(query.ManagerIdentityNotIn) > 0 || query.ManagedBy != "" {
columns = append(columns, &resourcepb.ResourceTableColumnDefinition{
Name: resource.SEARCH_FIELD_MANAGER_KIND,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
})
if query.ManagedBy != utils.ManagerKindPlugin && len(query.ManagerIdentityNotIn) == 0 {
columns = append(columns, []*resourcepb.ResourceTableColumnDefinition{
{
Name: resource.SEARCH_FIELD_MANAGER_ID,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
},
{
Name: resource.SEARCH_FIELD_SOURCE_PATH,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
},
{
Name: resource.SEARCH_FIELD_SOURCE_CHECKSUM,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
},
{
Name: resource.SEARCH_FIELD_SOURCE_TIME,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
},
}...)
}
return columns
}
// cannot sort when querying provisioned dashboards
if sortByField != "" {
columns = append(columns, &resourcepb.ResourceTableColumnDefinition{
Name: sortByField,
Type: resourcepb.ResourceTableColumnDefinition_INT64,
})
}
return columns
}
func (c *DashboardSearchClient) createCommonCells(title, folderUID string, id int64, tags []byte) [][]byte {
return [][]byte{
[]byte(title),
[]byte(folderUID),
tags,
[]byte(strconv.FormatInt(id, 10)),
}
}
func (c *DashboardSearchClient) createBaseCells(dashboard dashboards.DashboardSearchProjection, sortByField string) ([][]byte, error) {
tags, err := json.Marshal(dashboard.Tags)
if err != nil {
return nil, err
}
cells := c.createCommonCells(dashboard.Title, dashboard.FolderUID, dashboard.ID, tags)
if sortByField != "" {
cells = append(cells, []byte(strconv.FormatInt(dashboard.SortMeta, 10)))
}
return cells, nil
}
func (c *DashboardSearchClient) createProvisioningCells(dashboard *dashboards.Dashboard, query *dashboards.FindPersistedDashboardsQuery) [][]byte {
cells := c.createCommonCells(dashboard.Title, dashboard.FolderUID, dashboard.ID, []byte("[]"))
return append(cells, []byte(query.ManagedBy))
}
func (c *DashboardSearchClient) createDetailedProvisioningCells(dashboard *dashboards.DashboardProvisioningSearchResults, query *dashboards.FindPersistedDashboardsQuery) [][]byte {
cells := c.createCommonCells(dashboard.Dashboard.Title, dashboard.Dashboard.FolderUID, dashboard.Dashboard.ID, []byte("[]"))
return append(cells,
[]byte(query.ManagedBy),
[]byte(dashboard.Provisioner),
[]byte(dashboard.ExternalID),
[]byte(dashboard.CheckSum),
[]byte(strconv.FormatInt(dashboard.ProvisionUpdate, 10)),
)
}