diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 7f64599743b..aa994e3d44e 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -838,14 +838,14 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr if dashboardService == nil { dashboardService, err = service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, - ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, + ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(t, err) } dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, - ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, + ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(t, err) diff --git a/pkg/api/folder_bench_test.go b/pkg/api/folder_bench_test.go index 7dbe6b9e295..7901d005151 100644 --- a/pkg/api/folder_bench_test.go +++ b/pkg/api/folder_bench_test.go @@ -477,7 +477,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( sc.cfg, dashStore, folderStore, features, folderPermissions, dashboardPermissions, ac, - folderServiceWithFlagOn, fStore, nil, zanzana.NewNoopClient(), nil, nil, + folderServiceWithFlagOn, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(b, err) diff --git a/pkg/apimachinery/utils/meta.go b/pkg/apimachinery/utils/meta.go index a64d085912e..95270671384 100644 --- a/pkg/apimachinery/utils/meta.go +++ b/pkg/apimachinery/utils/meta.go @@ -34,8 +34,9 @@ const AnnoKeyRepoPath = "grafana.app/repoPath" const AnnoKeyRepoHash = "grafana.app/repoHash" const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp" +// LabelKeyDeprecatedInternalID gives the deprecated internal ID of a resource // Deprecated: will be removed in grafana 13 -const labelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID" +const LabelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID" // These can be removed once we verify that non of the dual-write sources // (for dashboards/playlists/etc) depend on the saved internal ID in SQL @@ -299,7 +300,7 @@ func (m *grafanaMetaAccessor) GetDeprecatedInternalID() int64 { return 0 } - if internalID, ok := labels[labelKeyDeprecatedInternalID]; ok { + if internalID, ok := labels[LabelKeyDeprecatedInternalID]; ok { id, err := strconv.ParseInt(internalID, 10, 64) if err == nil { return id @@ -316,7 +317,7 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) { // disallow setting it to 0 if id == 0 { if labels != nil { - delete(labels, labelKeyDeprecatedInternalID) + delete(labels, LabelKeyDeprecatedInternalID) m.obj.SetLabels(labels) } return @@ -326,7 +327,7 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) { labels = make(map[string]string) } - labels[labelKeyDeprecatedInternalID] = strconv.FormatInt(id, 10) + labels[LabelKeyDeprecatedInternalID] = strconv.FormatInt(id, 10) m.obj.SetLabels(labels) } diff --git a/pkg/registry/apis/dashboard/legacy/storage.go b/pkg/registry/apis/dashboard/legacy/storage.go index 4433ca89c20..e06a27ec43c 100644 --- a/pkg/registry/apis/dashboard/legacy/storage.go +++ b/pkg/registry/apis/dashboard/legacy/storage.go @@ -246,6 +246,7 @@ 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)") } diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index c0d6742baf8..5b92813eaea 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -18,6 +18,7 @@ import ( dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" + dashboardsvc "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/storage/unified/resource" "github.com/grafana/grafana/pkg/util/errhttp" ) @@ -308,46 +309,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { return } - sr := &dashboardv0alpha1.SearchResults{ - Offset: searchRequest.Offset, - TotalHits: result.TotalHits, - QueryCost: result.QueryCost, - MaxScore: result.MaxScore, - Hits: make([]dashboardv0alpha1.DashboardHit, len(result.Results.Rows)), - } - for i, row := range result.Results.Rows { - hit := &dashboardv0alpha1.DashboardHit{ - Resource: row.Key.Resource, // folders | dashboards - Name: row.Key.Name, // The Grafana UID - Title: string(row.Cells[0]), - Folder: string(row.Cells[1]), - } - if row.Cells[2] != nil { - _ = json.Unmarshal(row.Cells[2], &hit.Tags) - } - sr.Hits[i] = *hit - } - - // Add facet results - if result.Facet != nil { - sr.Facets = make(map[string]dashboardv0alpha1.FacetResult) - for k, v := range result.Facet { - sr.Facets[k] = dashboardv0alpha1.FacetResult{ - Field: v.Field, - Total: v.Total, - Missing: v.Missing, - Terms: make([]dashboardv0alpha1.TermFacet, len(v.Terms)), - } - for j, t := range v.Terms { - sr.Facets[k].Terms[j] = dashboardv0alpha1.TermFacet{ - Term: t.Term, - Count: t.Count, - } - } - } - } - - s.write(w, sr) + s.write(w, dashboardsvc.ParseResults(result, searchRequest.Offset)) } func (s *SearchHandler) write(w http.ResponseWriter, obj any) { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index d1836232f29..b27b5f45cd4 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -305,6 +305,7 @@ type SaveDashboardDTO struct { type DashboardSearchProjection struct { ID int64 `xorm:"id"` UID string `xorm:"uid"` + OrgID int64 `xorm:"org_id"` Title string Slug string Term string diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index caf967222f2..75367528081 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "time" @@ -27,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "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" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -42,6 +44,7 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity" "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/util" k8sUser "k8s.io/apiserver/pkg/authentication/user" k8sRequest "k8s.io/apiserver/pkg/endpoints/request" @@ -84,6 +87,7 @@ type DashboardServiceImpl struct { type dashboardK8sHandler interface { getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) getNamespace(orgID int64) string + getSearcher() resource.ResourceIndexClient } var _ dashboardK8sHandler = (*dashk8sHandler)(nil) @@ -92,6 +96,7 @@ type dashk8sHandler struct { namespacer request.NamespaceMapper gvr schema.GroupVersionResource restConfigProvider apiserver.RestConfigProvider + searcher resource.ResourceIndexClient } // This is the uber service that implements a three smaller services @@ -100,12 +105,13 @@ func ProvideDashboardServiceImpl( features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client, - restConfigProvider apiserver.RestConfigProvider, userService user.Service, + restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient, ) (*DashboardServiceImpl, error) { k8sHandler := &dashk8sHandler{ gvr: v0alpha1.DashboardResourceInfo.GroupVersionResource(), namespacer: request.GetNamespaceMapper(cfg), restConfigProvider: restConfigProvider, + searcher: unified, } dashSvc := &DashboardServiceImpl{ @@ -532,8 +538,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID} - // TODO: once we can do this search by IDs in unistore, remove this constraint - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) && cmd.UID != "" { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { return dr.deleteDashboardThroughK8s(ctx, cmd) } @@ -660,20 +665,54 @@ func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, } func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) { - // TODO: once we can do this search by ID in unistore, remove this constraint - if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) && query.UID != "" { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { return dr.getDashboardThroughK8s(ctx, query) } return dr.dashboardStore.GetDashboard(ctx, query) } -// TODO: once we can do this search by ID in unistore, go through k8s cli too func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + OrgId: requester.GetOrgID(), + DashboardIds: []int64{query.ID}, + }) + if err != nil { + return nil, err + } + + if len(result) != 1 { + return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result)) + } + + return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug}, nil + } + return dr.dashboardStore.GetDashboardUIDByID(ctx, query) } func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) { + if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) { + if query.OrgID == 0 { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + query.OrgID = requester.GetOrgID() + } + + return dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{ + DashboardIds: query.DashboardIDs, + OrgId: query.OrgID, + DashboardUIDs: query.DashboardUIDs, + }) + } + return dr.dashboardStore.GetDashboards(ctx, query) } @@ -769,7 +808,6 @@ func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context, return userDashboardUIDs, nil } -// TODO: once we can do this search by this in unistore, go through k8s cli too func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) { ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards") defer span.End() @@ -791,10 +829,39 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb dr.metrics.sharedWithMeFetchDashboardsRequestsDuration.WithLabelValues("success").Observe(time.Since(start).Seconds()) }(time.Now()) } + + if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesCliDashboards) { + if query.OrgId == 0 { + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + query.OrgId = requester.GetOrgID() + } + + results, err := dr.searchDashboardsThroughK8s(ctx, query) + if err != nil { + return nil, err + } + + finalResults := make([]dashboards.DashboardSearchProjection, len(results)) + for i, result := range results { + finalResults[i] = dashboards.DashboardSearchProjection{ + UID: result.UID, + OrgID: result.OrgID, + Title: result.Title, + Slug: result.Slug, + IsFolder: false, + FolderUID: result.FolderUID, + } + } + + return finalResults, nil + } + return dr.dashboardStore.FindDashboards(ctx, query) } -// TODO: once we can do this search in unistore, go through k8s cli too func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) { ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards") defer span.End() @@ -821,7 +888,7 @@ func (dr *DashboardServiceImpl) GetAllDashboards(ctx context.Context) ([]*dashbo if err != nil { return nil, err } - return dr.listDashboardThroughK8s(ctx, requester.GetOrgID()) + return dr.listDashboardsThroughK8s(ctx, requester.GetOrgID()) } return dr.dashboardStore.GetAllDashboards(ctx) @@ -840,15 +907,17 @@ func getHitType(item dashboards.DashboardSearchProjection) model.HitType { func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) model.HitList { hitList := make([]*model.Hit, 0) - hits := make(map[int64]*model.Hit) + hits := make(map[string]*model.Hit) for _, item := range res { - hit, exists := hits[item.ID] + key := fmt.Sprintf("%s-%d", item.UID, item.OrgID) + hit, exists := hits[key] if !exists { metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() hit = &model.Hit{ ID: item.ID, UID: item.UID, + OrgID: item.OrgID, Title: item.Title, URI: "db/" + item.Slug, URL: dashboards.GetDashboardFolderURL(item.IsFolder, item.UID, item.Slug), @@ -870,7 +939,7 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb } hitList = append(hitList, hit) - hits[item.ID] = hit + hits[key] = hit } if len(item.Term) > 0 { hit.Tags = append(hit.Tags, item.Term) @@ -943,6 +1012,10 @@ func (dk8s *dashk8sHandler) getNamespace(orgID int64) string { return dk8s.namespacer(orgID) } +func (dk8s *dashk8sHandler) getSearcher() resource.ResourceIndexClient { + return dk8s.searcher +} + func (dr *DashboardServiceImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) { requester, requesterErr := identity.GetRequester(ctx) if requesterErr != nil { @@ -1000,6 +1073,18 @@ func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, quer subresource = "latest" } + // get uid if not passed in + if query.UID == "" { + result, err := dr.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{ + ID: query.ID, + }) + if err != nil { + return nil, err + } + + query.UID = result.UID + } + out, err := client.Get(newCtx, query.UID, v1.GetOptions{}, subresource) if err != nil { return nil, err @@ -1067,6 +1152,18 @@ func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, c return fmt.Errorf("could not get k8s client") } + // get uid if not passed in + if cmd.UID == "" { + result, err := dr.GetDashboardUIDByID(ctx, &dashboards.GetDashboardRefByIDQuery{ + ID: cmd.ID, + }) + if err != nil { + return err + } + + cmd.UID = result.UID + } + err = client.Delete(newCtx, cmd.UID, v1.DeleteOptions{}) if err != nil { return err @@ -1075,7 +1172,7 @@ func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, c return nil } -func (dr *DashboardServiceImpl) listDashboardThroughK8s(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) { +func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, 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 := dr.getK8sContext(ctx) @@ -1090,7 +1187,6 @@ func (dr *DashboardServiceImpl) listDashboardThroughK8s(ctx context.Context, org return nil, nil } - // TODO: once we can do this search in unistore, update this out, err := client.List(newCtx, v1.ListOptions{}) if err != nil { return nil, err @@ -1110,6 +1206,164 @@ func (dr *DashboardServiceImpl) listDashboardThroughK8s(ctx context.Context, org return dashboards, nil } +func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) { + dashboardskey := &resource.ResourceKey{ + Namespace: dr.k8sclient.getNamespace(query.OrgId), + Group: "dashboard.grafana.app", + Resource: "dashboards", + } + + request := &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: dashboardskey, + }, + Limit: 100000} + + if len(query.DashboardUIDs) > 0 { + request.Options.Fields = []*resource.Requirement{{ + Key: "key.name", + Operator: "in", + Values: query.DashboardUIDs, + }} + } else if len(query.DashboardIds) > 0 { + values := make([]string, len(query.DashboardIds)) + for _, id := range query.DashboardIds { + values = append(values, strconv.FormatInt(id, 10)) + } + + request.Options.Labels = []*resource.Requirement{{ + Key: utils.LabelKeyDeprecatedInternalID, // nolint:staticcheck + Operator: "in", + Values: values, + }} + } + + if len(query.FolderUIDs) > 0 { + req := []*resource.Requirement{{ + Key: "folder", + Operator: "in", + Values: query.FolderUIDs, + }} + if len(request.Options.Fields) == 0 { + request.Options.Fields = req + } else { + request.Options.Fields = append(request.Options.Fields, req...) + } + } + + // note: this does not allow for partial matching + // + // partial matching will be allowed through the api layer for the frontend, + // but is currently not needed by other services in the backend + if query.Title != "" { + req := []*resource.Requirement{{ + Key: "title", + Operator: "in", + Values: []string{query.Title}, + }} + if len(request.Options.Fields) == 0 { + request.Options.Fields = req + } else { + request.Options.Fields = append(request.Options.Fields, req...) + } + } + + if len(query.Tags) > 0 { + req := []*resource.Requirement{{ + Key: "tags", + Operator: "in", + Values: query.Tags, + }} + + if len(request.Options.Fields) == 0 { + request.Options.Fields = req + } else { + request.Options.Fields = append(request.Options.Fields, req...) + } + } + + res, err := dr.k8sclient.getSearcher().Search(ctx, request) + if err != nil { + return nil, err + } + + response := ParseResults(res, 0) + result := make([]*dashboards.Dashboard, len(response.Hits)) + for i, hit := range response.Hits { + result[i] = &dashboards.Dashboard{ + OrgID: query.OrgId, + UID: hit.Name, + Slug: slugify.Slugify(hit.Title), + Title: hit.Title, + FolderUID: hit.Folder, + } + } + + return result, nil +} + +func ParseResults(result *resource.ResourceSearchResponse, offset int64) *v0alpha1.SearchResults { + if result == nil { + return nil + } + + sr := &v0alpha1.SearchResults{ + Offset: offset, + TotalHits: result.TotalHits, + QueryCost: result.QueryCost, + MaxScore: result.MaxScore, + Hits: make([]v0alpha1.DashboardHit, len(result.Results.Rows)), + } + + titleRow := 0 + folderRow := 1 + tagsRow := -1 + for i, row := range result.Results.GetColumns() { + if row.Name == "title" { + titleRow = i + } else if row.Name == "folder" { + folderRow = i + } else if row.Name == "tags" { + tagsRow = i + } + } + + for i, row := range result.Results.Rows { + hit := &v0alpha1.DashboardHit{ + Resource: row.Key.Resource, // folders | dashboards + Name: row.Key.Name, // The Grafana UID + Title: string(row.Cells[titleRow]), + Folder: string(row.Cells[folderRow]), + } + if tagsRow != -1 && row.Cells[tagsRow] != nil { + _ = json.Unmarshal(row.Cells[tagsRow], &hit.Tags) + } + + sr.Hits[i] = *hit + } + + // Add facet results + if result.Facet != nil { + sr.Facets = make(map[string]v0alpha1.FacetResult) + for k, v := range result.Facet { + sr.Facets[k] = v0alpha1.FacetResult{ + Field: v.Field, + Total: v.Total, + Missing: v.Missing, + Terms: make([]v0alpha1.TermFacet, len(v.Terms)), + } + for j, t := range v.Terms { + sr.Facets[k].Terms[j] = v0alpha1.TermFacet{ + Term: t.Term, + Count: t.Count, + } + } + } + } + + return sr +} + func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) { spec, ok := item.Object["spec"].(map[string]any) if !ok { diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 7a86c7ea47f..19c4ffef3c2 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -887,6 +887,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) guardian.InitAccessControlGuardian(cfg, ac, dashboardService) @@ -956,6 +957,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) res, err := service.SaveDashboard(context.Background(), &dto, false) @@ -984,6 +986,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) _, err = service.SaveDashboard(context.Background(), &dto, false) @@ -1031,6 +1034,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) res, err := service.SaveDashboard(context.Background(), &dto, false) @@ -1085,6 +1089,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) res, err := service.SaveDashboard(context.Background(), &dto, false) diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 5a852641cac..7427fa4d1b9 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "google.golang.org/grpc" "k8s.io/client-go/dynamic" "github.com/grafana/grafana/pkg/apimachinery/identity" @@ -20,9 +21,11 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -234,6 +237,7 @@ func TestDashboardService(t *testing.T) { type mockDashK8sCli struct { mock.Mock + searcher *mockResourceIndexClient } func (m *mockDashK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { @@ -245,6 +249,20 @@ func (m *mockDashK8sCli) getNamespace(orgID int64) string { return "default" } +func (m *mockDashK8sCli) getSearcher() resource.ResourceIndexClient { + return m.searcher +} + +type mockResourceIndexClient struct { + mock.Mock + resource.ResourceIndexClient +} + +func (m *mockResourceIndexClient) Search(ctx context.Context, req *resource.ResourceSearchRequest, opts ...grpc.CallOption) (*resource.ResourceSearchResponse, error) { + args := m.Called(req) + return args.Get(0).(*resource.ResourceSearchResponse), args.Error(1) +} + type mockResourceInterface struct { mock.Mock dynamic.ResourceInterface @@ -290,6 +308,7 @@ func (m *mockResourceInterface) Delete(ctx context.Context, name string, options func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *mockDashK8sCli, *mockResourceInterface) { k8sClientMock := new(mockDashK8sCli) k8sResourceMock := new(mockResourceInterface) + k8sClientMock.searcher = new(mockResourceIndexClient) service.k8sclient = k8sClientMock service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) @@ -353,6 +372,69 @@ func TestGetDashboard(t *testing.T) { require.True(t, reflect.DeepEqual(dashboard, &dashboardExpected)) }) + t.Run("Should get uid if not passed in at first", func(t *testing.T) { + query := &dashboards.GetDashboardQuery{ + ID: 1, + UID: "", + OrgID: 1, + } + ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) + dashboardUnstructured := unstructured.Unstructured{Object: map[string]any{ + "metadata": map[string]any{ + "name": "uid", + }, + "spec": map[string]any{ + "test": "test", + "version": int64(1), + "title": "testing slugify", + }, + }} + + dashboardExpected := dashboards.Dashboard{ + UID: "uid", // uid is the name of the k8s object + Title: "testing slugify", + Slug: "testing-slugify", // slug is taken from title + OrgID: 1, // orgID is populated from the query + Version: 1, + Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}), + } + k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() + k8sResourceMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once() + k8sClientMock.searcher.On("Search", mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + }, + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + + dashboard, err := service.GetDashboard(ctx, query) + require.NoError(t, err) + require.NotNil(t, dashboard) + k8sClientMock.AssertExpectations(t) + k8sClientMock.searcher.AssertExpectations(t) + // make sure the conversion is working + require.True(t, reflect.DeepEqual(dashboard, &dashboardExpected)) + }) + t.Run("Should return error when Kubernetes client fails", func(t *testing.T) { ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() @@ -524,6 +606,302 @@ func TestDeleteDashboard(t *testing.T) { require.NoError(t, err) k8sClientMock.AssertExpectations(t) }) + + t.Run("If UID is not passed in, it should retrieve that first", func(t *testing.T) { + ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) + k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() + fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil).Once() + k8sResourceMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + k8sClientMock.searcher.On("Search", mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + }, + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + err := service.DeleteDashboard(ctx, 1, "", 1) + require.NoError(t, err) + k8sClientMock.AssertExpectations(t) + k8sClientMock.searcher.AssertExpectations(t) + }) +} + +func TestSearchDashboards(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + } + + expectedResult := model.HitList{ + { + UID: "uid1", + OrgID: 1, + Title: "Dashboard 1", + Type: "dash-db", + URI: "db/dashboard-1", + URL: "/d/uid1/dashboard-1", + Tags: []string{}, + }, + { + UID: "uid2", + OrgID: 1, + Title: "Dashboard 2", + Type: "dash-db", + URI: "db/dashboard-2", + URL: "/d/uid2/dashboard-2", + Tags: []string{}, + }, + } + query := dashboards.FindPersistedDashboardsQuery{ + DashboardUIDs: []string{"uid1", "uid2"}, + } + t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { + service.features = featuremgmt.WithFeatures() + fakeStore.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{ + { + UID: "uid1", + Slug: "dashboard-1", + OrgID: 1, + Title: "Dashboard 1", + }, + { + UID: "uid2", + Slug: "dashboard-2", + OrgID: 1, + Title: "Dashboard 2", + }, + }, nil).Once() + result, err := service.SearchDashboards(context.Background(), &query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + fakeStore.AssertExpectations(t) + }) + + t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) + k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() + k8sClientMock.searcher.On("Search", mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + }, + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid1", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte(""), + }, + }, + { + Key: &resource.ResourceKey{ + Name: "uid2", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 2"), + []byte(""), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + + result, err := service.SearchDashboards(ctx, &query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sClientMock.searcher.AssertExpectations(t) + }) +} + +func TestGetDashboards(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + } + + expectedResult := []*dashboards.Dashboard{ + { + UID: "uid1", + Slug: "dashboard-1", + OrgID: 1, + Title: "Dashboard 1", + }, + { + UID: "uid2", + Slug: "dashboard-2", + OrgID: 1, + Title: "Dashboard 2", + }, + } + queryByIDs := &dashboards.GetDashboardsQuery{ + DashboardIDs: []int64{1, 2}, + OrgID: 1, + } + queryByUIDs := &dashboards.GetDashboardsQuery{ + DashboardUIDs: []string{"uid1", "uid2"}, + OrgID: 1, + } + t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { + service.features = featuremgmt.WithFeatures() + + // by ids + fakeStore.On("GetDashboards", mock.Anything, queryByIDs).Return(expectedResult, nil).Once() + result, err := service.GetDashboards(context.Background(), queryByIDs) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + fakeStore.AssertExpectations(t) + + // by uids + fakeStore.On("GetDashboards", mock.Anything, queryByUIDs).Return(expectedResult, nil).Once() + result, err = service.GetDashboards(context.Background(), queryByUIDs) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + fakeStore.AssertExpectations(t) + }) + + t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) + k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() + k8sClientMock.searcher.On("Search", mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + }, + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid1", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte(""), + }, + }, + { + Key: &resource.ResourceKey{ + Name: "uid2", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 2"), + []byte(""), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + + // by ids + result, err := service.GetDashboards(ctx, queryByIDs) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sClientMock.searcher.AssertExpectations(t) + + // by uids + result, err = service.GetDashboards(ctx, queryByUIDs) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sClientMock.searcher.AssertExpectations(t) + }) +} + +func TestGetDashboardUIDByID(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + } + + expectedResult := &dashboards.DashboardRef{ + UID: "uid1", + Slug: "dashboard-1", + } + query := &dashboards.GetDashboardRefByIDQuery{ + ID: 1, + } + t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) { + service.features = featuremgmt.WithFeatures() + fakeStore.On("GetDashboardUIDByID", mock.Anything, query).Return(expectedResult, nil).Once() + + result, err := service.GetDashboardUIDByID(context.Background(), query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + fakeStore.AssertExpectations(t) + }) + + t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) { + ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) + k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() + k8sClientMock.searcher.On("Search", mock.Anything).Return(&resource.ResourceSearchResponse{ + Results: &resource.ResourceTable{ + Columns: []*resource.ResourceTableColumnDefinition{ + { + Name: "title", + }, + { + Name: "folder", + }, + }, + Rows: []*resource.ResourceTableRow{ + { + Key: &resource.ResourceKey{ + Name: "uid1", + Resource: "dashboard", + }, + Cells: [][]byte{ + []byte("Dashboard 1"), + []byte("folder1"), + }, + }, + }, + }, + TotalHits: 1, + }, nil) + result, err := service.GetDashboardUIDByID(ctx, query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + k8sClientMock.searcher.AssertExpectations(t) + }) } func TestUnstructuredToLegacyDashboard(t *testing.T) { diff --git a/pkg/services/dashboards/service/zanzana_integration_test.go b/pkg/services/dashboards/service/zanzana_integration_test.go index 359b8bcebc5..fe7d9971bca 100644 --- a/pkg/services/dashboards/service/zanzana_integration_test.go +++ b/pkg/services/dashboards/service/zanzana_integration_test.go @@ -75,6 +75,7 @@ func TestIntegrationDashboardServiceZanzana(t *testing.T) { zclient, nil, nil, + nil, ) require.NoError(t, err) diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index 48b996c7f06..b1429cbbc18 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -101,7 +101,7 @@ func TestValidateDashboardExists(t *testing.T) { feats := featuremgmt.WithFeatures() dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, feats, tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) require.NoError(t, err) - dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), feats, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil, nil) + dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), feats, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil, nil, nil) require.NoError(t, err) s := ProvideService(dsStore, secretsService, dashSvc) ctx := context.Background() diff --git a/pkg/services/folder/folderimpl/folder_test.go b/pkg/services/folder/folderimpl/folder_test.go index acb6635de0f..2f97d521fae 100644 --- a/pkg/services/folder/folderimpl/folder_test.go +++ b/pkg/services/folder/folderimpl/folder_test.go @@ -487,7 +487,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { CanEditValue: true, }) - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, zanzana.NewNoopClient(), nil, nil) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, zanzana.NewNoopClient(), nil, nil, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b) @@ -569,7 +569,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { }) dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff, - folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, zanzana.NewNoopClient(), nil, nil) + folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, zanzana.NewNoopClient(), nil, nil, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b) @@ -714,7 +714,7 @@ func TestIntegrationNestedFolderService(t *testing.T) { tc.service.dashboardStore = dashStore tc.service.store = nestedFolderStore - dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil, zanzana.NewNoopClient(), nil, nil) + dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil, zanzana.NewNoopClient(), nil, nil, nil) require.NoError(t, err) alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b) require.NoError(t, err) @@ -1499,6 +1499,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) { zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 7fa6c32b3a2..76faa196e53 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -313,6 +313,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, err) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) @@ -401,6 +402,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena nil, zanzana.NewNoopClient(), nil, nil, + nil, ) require.NoError(t, svcErr) guardian.InitAccessControlGuardian(cfg, ac, dashboardService) @@ -462,7 +464,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, zanzana.NewNoopClient(), nil, nil, + nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(t, dashSvcErr) guardian.InitAccessControlGuardian(cfg, ac, dashService) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 4e632d75069..a83b1b38735 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -735,7 +735,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, zanzana.NewNoopClient(), nil, nil, + nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(t, err) dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) @@ -831,7 +831,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo cfg, dashStore, folderStore, features, acmock.NewMockedPermissionsService(), dashPermissionService, ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, zanzana.NewNoopClient(), nil, nil, + nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(t, err) guardian.InitAccessControlGuardian(cfg, ac, dashService) diff --git a/pkg/services/ngalert/testutil/testutil.go b/pkg/services/ngalert/testutil/testutil.go index e7a13b6c4fd..cca3f9dc94a 100644 --- a/pkg/services/ngalert/testutil/testutil.go +++ b/pkg/services/ngalert/testutil/testutil.go @@ -61,7 +61,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa cfg, dashboardStore, fs, features, folderPermissions, dashboardPermissions, ac, foldertest.NewFakeService(), folder.NewFakeStore(), - nil, zanzana.NewNoopClient(), nil, nil, + nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(tb, err) diff --git a/pkg/services/publicdashboards/api/query_test.go b/pkg/services/publicdashboards/api/query_test.go index 775f367d516..23a4a706dbf 100644 --- a/pkg/services/publicdashboards/api/query_test.go +++ b/pkg/services/publicdashboards/api/query_test.go @@ -326,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) dashService, err := service.ProvideDashboardServiceImpl( cfg, dashboardStoreService, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, - foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil, nil, + foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil, nil, nil, ) require.NoError(t, err) diff --git a/pkg/services/search/model/model.go b/pkg/services/search/model/model.go index 6b95d2a9448..afad2f0ff5d 100644 --- a/pkg/services/search/model/model.go +++ b/pkg/services/search/model/model.go @@ -65,6 +65,7 @@ const ( type Hit struct { ID int64 `json:"id"` UID string `json:"uid"` + OrgID int64 `json:"orgId"` Title string `json:"title"` URI string `json:"uri"` URL string `json:"url"` diff --git a/pkg/services/sqlstore/searchstore/builder.go b/pkg/services/sqlstore/searchstore/builder.go index fc318ffe0e0..ec7f6ca3f8a 100644 --- a/pkg/services/sqlstore/searchstore/builder.go +++ b/pkg/services/sqlstore/searchstore/builder.go @@ -60,6 +60,7 @@ func (b *Builder) buildSelect() { b.sql.WriteString( `SELECT dashboard.id, + dashboard.org_id, dashboard.uid, dashboard.title, dashboard.slug, diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index 33dcc0f3402..7bccffef1d6 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -68,6 +68,7 @@ func TestBuilder_EqualResults_Basic(t *testing.T) { { ID: dashIds[0], Title: "A", + OrgID: 1, Slug: "a", Term: "templated", }, diff --git a/public/api-merged.json b/public/api-merged.json index 5f2e962b409..76805351bc0 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -16413,6 +16413,10 @@ "isStarred": { "type": "boolean" }, + "orgId": { + "type": "integer", + "format": "int64" + }, "permanentlyDeleteDate": { "type": "string", "format": "date-time" diff --git a/public/openapi3.json b/public/openapi3.json index 5bba4ec6ad8..7555acb8621 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -6487,6 +6487,10 @@ "isStarred": { "type": "boolean" }, + "orgId": { + "format": "int64", + "type": "integer" + }, "permanentlyDeleteDate": { "format": "date-time", "type": "string"