K8s: Dashboards: Add search to dashboard service (#98395)

adela/datalinks_one_click
Stephanie Hingtgen 7 months ago committed by GitHub
parent 81ca734680
commit 03f7a7d89d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      pkg/api/dashboard_test.go
  2. 2
      pkg/api/folder_bench_test.go
  3. 9
      pkg/apimachinery/utils/meta.go
  4. 1
      pkg/registry/apis/dashboard/legacy/storage.go
  5. 42
      pkg/registry/apis/dashboard/search.go
  6. 1
      pkg/services/dashboards/models.go
  7. 282
      pkg/services/dashboards/service/dashboard_service.go
  8. 5
      pkg/services/dashboards/service/dashboard_service_integration_test.go
  9. 378
      pkg/services/dashboards/service/dashboard_service_test.go
  10. 1
      pkg/services/dashboards/service/zanzana_integration_test.go
  11. 2
      pkg/services/dashboardsnapshots/service/service_test.go
  12. 7
      pkg/services/folder/folderimpl/folder_test.go
  13. 4
      pkg/services/libraryelements/libraryelements_test.go
  14. 4
      pkg/services/librarypanels/librarypanels_test.go
  15. 2
      pkg/services/ngalert/testutil/testutil.go
  16. 2
      pkg/services/publicdashboards/api/query_test.go
  17. 1
      pkg/services/search/model/model.go
  18. 1
      pkg/services/sqlstore/searchstore/builder.go
  19. 1
      pkg/services/sqlstore/searchstore/search_test.go
  20. 4
      public/api-merged.json
  21. 4
      public/openapi3.json

@ -838,14 +838,14 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
if dashboardService == nil { if dashboardService == nil {
dashboardService, err = service.ProvideDashboardServiceImpl( dashboardService, err = service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, 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) require.NoError(t, err)
} }
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl( dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions, 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) require.NoError(t, err)

@ -477,7 +477,7 @@ func setupServer(b testing.TB, sc benchScenario, features featuremgmt.FeatureTog
dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl( dashboardSvc, err := dashboardservice.ProvideDashboardServiceImpl(
sc.cfg, dashStore, folderStore, sc.cfg, dashStore, folderStore,
features, folderPermissions, dashboardPermissions, ac, features, folderPermissions, dashboardPermissions, ac,
folderServiceWithFlagOn, fStore, nil, zanzana.NewNoopClient(), nil, nil, folderServiceWithFlagOn, fStore, nil, zanzana.NewNoopClient(), nil, nil, nil,
) )
require.NoError(b, err) require.NoError(b, err)

@ -34,8 +34,9 @@ const AnnoKeyRepoPath = "grafana.app/repoPath"
const AnnoKeyRepoHash = "grafana.app/repoHash" const AnnoKeyRepoHash = "grafana.app/repoHash"
const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp" const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"
// LabelKeyDeprecatedInternalID gives the deprecated internal ID of a resource
// Deprecated: will be removed in grafana 13 // 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 // 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 // (for dashboards/playlists/etc) depend on the saved internal ID in SQL
@ -299,7 +300,7 @@ func (m *grafanaMetaAccessor) GetDeprecatedInternalID() int64 {
return 0 return 0
} }
if internalID, ok := labels[labelKeyDeprecatedInternalID]; ok { if internalID, ok := labels[LabelKeyDeprecatedInternalID]; ok {
id, err := strconv.ParseInt(internalID, 10, 64) id, err := strconv.ParseInt(internalID, 10, 64)
if err == nil { if err == nil {
return id return id
@ -316,7 +317,7 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) {
// disallow setting it to 0 // disallow setting it to 0
if id == 0 { if id == 0 {
if labels != nil { if labels != nil {
delete(labels, labelKeyDeprecatedInternalID) delete(labels, LabelKeyDeprecatedInternalID)
m.obj.SetLabels(labels) m.obj.SetLabels(labels)
} }
return return
@ -326,7 +327,7 @@ func (m *grafanaMetaAccessor) SetDeprecatedInternalID(id int64) {
labels = make(map[string]string) labels = make(map[string]string)
} }
labels[labelKeyDeprecatedInternalID] = strconv.FormatInt(id, 10) labels[LabelKeyDeprecatedInternalID] = strconv.FormatInt(id, 10)
m.obj.SetLabels(labels) m.obj.SetLabels(labels)
} }

@ -246,6 +246,7 @@ 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 nil, fmt.Errorf("not yet (filter)")
} }

@ -18,6 +18,7 @@ import (
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1" dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"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"
dashboardsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
"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"
) )
@ -308,46 +309,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) {
return return
} }
sr := &dashboardv0alpha1.SearchResults{ s.write(w, dashboardsvc.ParseResults(result, searchRequest.Offset))
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)
} }
func (s *SearchHandler) write(w http.ResponseWriter, obj any) { func (s *SearchHandler) write(w http.ResponseWriter, obj any) {

@ -305,6 +305,7 @@ type SaveDashboardDTO struct {
type DashboardSearchProjection struct { type DashboardSearchProjection struct {
ID int64 `xorm:"id"` ID int64 `xorm:"id"`
UID string `xorm:"uid"` UID string `xorm:"uid"`
OrgID int64 `xorm:"org_id"`
Title string Title string
Slug string Slug string
Term string Term string

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"time" "time"
@ -27,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics" "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/accesscontrol"
"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"
@ -42,6 +44,7 @@ import (
"github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/entity"
"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/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
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"
@ -84,6 +87,7 @@ type DashboardServiceImpl struct {
type dashboardK8sHandler interface { type dashboardK8sHandler interface {
getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
getNamespace(orgID int64) string getNamespace(orgID int64) string
getSearcher() resource.ResourceIndexClient
} }
var _ dashboardK8sHandler = (*dashk8sHandler)(nil) var _ dashboardK8sHandler = (*dashk8sHandler)(nil)
@ -92,6 +96,7 @@ type dashk8sHandler struct {
namespacer request.NamespaceMapper namespacer request.NamespaceMapper
gvr schema.GroupVersionResource gvr schema.GroupVersionResource
restConfigProvider apiserver.RestConfigProvider restConfigProvider apiserver.RestConfigProvider
searcher resource.ResourceIndexClient
} }
// This is the uber service that implements a three smaller services // This is the uber service that implements a three smaller services
@ -100,12 +105,13 @@ func ProvideDashboardServiceImpl(
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService, features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl, dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client, 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) { ) (*DashboardServiceImpl, error) {
k8sHandler := &dashk8sHandler{ k8sHandler := &dashk8sHandler{
gvr: v0alpha1.DashboardResourceInfo.GroupVersionResource(), gvr: v0alpha1.DashboardResourceInfo.GroupVersionResource(),
namespacer: request.GetNamespaceMapper(cfg), namespacer: request.GetNamespaceMapper(cfg),
restConfigProvider: restConfigProvider, restConfigProvider: restConfigProvider,
searcher: unified,
} }
dashSvc := &DashboardServiceImpl{ dashSvc := &DashboardServiceImpl{
@ -532,8 +538,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID} 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) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) && cmd.UID != "" {
return dr.deleteDashboardThroughK8s(ctx, cmd) 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) { 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) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) && query.UID != "" {
return dr.getDashboardThroughK8s(ctx, query) return dr.getDashboardThroughK8s(ctx, query)
} }
return dr.dashboardStore.GetDashboard(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) { 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) return dr.dashboardStore.GetDashboardUIDByID(ctx, query)
} }
func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) { 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) return dr.dashboardStore.GetDashboards(ctx, query)
} }
@ -769,7 +808,6 @@ func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context,
return userDashboardUIDs, nil 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) { func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards") ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards")
defer span.End() 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()) dr.metrics.sharedWithMeFetchDashboardsRequestsDuration.WithLabelValues("success").Observe(time.Since(start).Seconds())
}(time.Now()) }(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) 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) { func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards") ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards")
defer span.End() defer span.End()
@ -821,7 +888,7 @@ func (dr *DashboardServiceImpl) GetAllDashboards(ctx context.Context) ([]*dashbo
if err != nil { if err != nil {
return nil, err return nil, err
} }
return dr.listDashboardThroughK8s(ctx, requester.GetOrgID()) return dr.listDashboardsThroughK8s(ctx, requester.GetOrgID())
} }
return dr.dashboardStore.GetAllDashboards(ctx) 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 { func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashboards.DashboardSearchProjection) model.HitList {
hitList := make([]*model.Hit, 0) hitList := make([]*model.Hit, 0)
hits := make(map[int64]*model.Hit) hits := make(map[string]*model.Hit)
for _, item := range res { for _, item := range res {
hit, exists := hits[item.ID] key := fmt.Sprintf("%s-%d", item.UID, item.OrgID)
hit, exists := hits[key]
if !exists { if !exists {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
hit = &model.Hit{ hit = &model.Hit{
ID: item.ID, ID: item.ID,
UID: item.UID, UID: item.UID,
OrgID: item.OrgID,
Title: item.Title, Title: item.Title,
URI: "db/" + item.Slug, URI: "db/" + item.Slug,
URL: dashboards.GetDashboardFolderURL(item.IsFolder, item.UID, 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) hitList = append(hitList, hit)
hits[item.ID] = hit hits[key] = hit
} }
if len(item.Term) > 0 { if len(item.Term) > 0 {
hit.Tags = append(hit.Tags, item.Term) hit.Tags = append(hit.Tags, item.Term)
@ -943,6 +1012,10 @@ func (dk8s *dashk8sHandler) getNamespace(orgID int64) string {
return dk8s.namespacer(orgID) 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) { func (dr *DashboardServiceImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) {
requester, requesterErr := identity.GetRequester(ctx) requester, requesterErr := identity.GetRequester(ctx)
if requesterErr != nil { if requesterErr != nil {
@ -1000,6 +1073,18 @@ func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, quer
subresource = "latest" 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) out, err := client.Get(newCtx, query.UID, v1.GetOptions{}, subresource)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1067,6 +1152,18 @@ func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, c
return fmt.Errorf("could not get k8s client") 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{}) err = client.Delete(newCtx, cmd.UID, v1.DeleteOptions{})
if err != nil { if err != nil {
return err return err
@ -1075,7 +1172,7 @@ func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, c
return nil 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 // 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 // otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx) newCtx, cancel, err := dr.getK8sContext(ctx)
@ -1090,7 +1187,6 @@ func (dr *DashboardServiceImpl) listDashboardThroughK8s(ctx context.Context, org
return nil, nil return nil, nil
} }
// TODO: once we can do this search in unistore, update this
out, err := client.List(newCtx, v1.ListOptions{}) out, err := client.List(newCtx, v1.ListOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
@ -1110,6 +1206,164 @@ func (dr *DashboardServiceImpl) listDashboardThroughK8s(ctx context.Context, org
return dashboards, nil 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) { func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) {
spec, ok := item.Object["spec"].(map[string]any) spec, ok := item.Object["spec"].(map[string]any)
if !ok { if !ok {

@ -887,6 +887,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
zanzana.NewNoopClient(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)
guardian.InitAccessControlGuardian(cfg, ac, dashboardService) guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
@ -956,6 +957,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
zanzana.NewNoopClient(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false) res, err := service.SaveDashboard(context.Background(), &dto, false)
@ -984,6 +986,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
zanzana.NewNoopClient(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)
_, err = service.SaveDashboard(context.Background(), &dto, false) _, err = service.SaveDashboard(context.Background(), &dto, false)
@ -1031,6 +1034,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
zanzana.NewNoopClient(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false) 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(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false) res, err := service.SaveDashboard(context.Background(), &dto, false)

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"github.com/grafana/grafana/pkg/apimachinery/identity" "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"
"github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian" "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"
"github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
@ -234,6 +237,7 @@ func TestDashboardService(t *testing.T) {
type mockDashK8sCli struct { type mockDashK8sCli struct {
mock.Mock mock.Mock
searcher *mockResourceIndexClient
} }
func (m *mockDashK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) { 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" 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 { type mockResourceInterface struct {
mock.Mock mock.Mock
dynamic.ResourceInterface dynamic.ResourceInterface
@ -290,6 +308,7 @@ func (m *mockResourceInterface) Delete(ctx context.Context, name string, options
func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *mockDashK8sCli, *mockResourceInterface) { func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *mockDashK8sCli, *mockResourceInterface) {
k8sClientMock := new(mockDashK8sCli) k8sClientMock := new(mockDashK8sCli)
k8sResourceMock := new(mockResourceInterface) k8sResourceMock := new(mockResourceInterface)
k8sClientMock.searcher = new(mockResourceIndexClient)
service.k8sclient = k8sClientMock service.k8sclient = k8sClientMock
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards) service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
@ -353,6 +372,69 @@ func TestGetDashboard(t *testing.T) {
require.True(t, reflect.DeepEqual(dashboard, &dashboardExpected)) 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) { t.Run("Should return error when Kubernetes client fails", func(t *testing.T) {
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service) ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once() k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once()
@ -524,6 +606,302 @@ func TestDeleteDashboard(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
k8sClientMock.AssertExpectations(t) 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) { func TestUnstructuredToLegacyDashboard(t *testing.T) {

@ -75,6 +75,7 @@ func TestIntegrationDashboardServiceZanzana(t *testing.T) {
zclient, zclient,
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)

@ -101,7 +101,7 @@ func TestValidateDashboardExists(t *testing.T) {
feats := featuremgmt.WithFeatures() feats := featuremgmt.WithFeatures()
dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, feats, tagimpl.ProvideService(sqlStore), quotatest.New(false, nil)) dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, feats, tagimpl.ProvideService(sqlStore), quotatest.New(false, nil))
require.NoError(t, err) 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) require.NoError(t, err)
s := ProvideService(dsStore, secretsService, dashSvc) s := ProvideService(dsStore, secretsService, dashSvc)
ctx := context.Background() ctx := context.Background()

@ -487,7 +487,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
CanEditValue: true, 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) require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b) 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, 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) require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b) 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.dashboardStore = dashStore
tc.service.store = nestedFolderStore 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) require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b) alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b)
require.NoError(t, err) require.NoError(t, err)
@ -1499,6 +1499,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
zanzana.NewNoopClient(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)

@ -313,6 +313,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
zanzana.NewNoopClient(), zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, err) require.NoError(t, err)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) 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, zanzana.NewNoopClient(),
nil, nil,
nil, nil,
nil,
) )
require.NoError(t, svcErr) require.NoError(t, svcErr)
guardian.InitAccessControlGuardian(cfg, ac, dashboardService) 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, cfg, dashboardStore, folderStore,
features, folderPermissions, dashboardPermissions, ac, features, folderPermissions, dashboardPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(), foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil, nil, nil, zanzana.NewNoopClient(), nil, nil, nil,
) )
require.NoError(t, dashSvcErr) require.NoError(t, dashSvcErr)
guardian.InitAccessControlGuardian(cfg, ac, dashService) guardian.InitAccessControlGuardian(cfg, ac, dashService)

@ -735,7 +735,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
cfg, dashboardStore, folderStore, cfg, dashboardStore, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(), foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil, nil, nil, zanzana.NewNoopClient(), nil, nil, nil,
) )
require.NoError(t, err) require.NoError(t, err)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true) 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, cfg, dashStore, folderStore,
features, acmock.NewMockedPermissionsService(), dashPermissionService, ac, features, acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(), foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil, nil, nil, zanzana.NewNoopClient(), nil, nil, nil,
) )
require.NoError(t, err) require.NoError(t, err)
guardian.InitAccessControlGuardian(cfg, ac, dashService) guardian.InitAccessControlGuardian(cfg, ac, dashService)

@ -61,7 +61,7 @@ func SetupDashboardService(tb testing.TB, sqlStore db.DB, fs *folderimpl.Dashboa
cfg, dashboardStore, fs, cfg, dashboardStore, fs,
features, folderPermissions, dashboardPermissions, ac, features, folderPermissions, dashboardPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(), foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil, nil, nil, zanzana.NewNoopClient(), nil, nil, nil,
) )
require.NoError(tb, err) require.NoError(tb, err)

@ -326,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
dashService, err := service.ProvideDashboardServiceImpl( dashService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStoreService, folderStore, cfg, dashboardStoreService, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac, 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) require.NoError(t, err)

@ -65,6 +65,7 @@ const (
type Hit struct { type Hit struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UID string `json:"uid"` UID string `json:"uid"`
OrgID int64 `json:"orgId"`
Title string `json:"title"` Title string `json:"title"`
URI string `json:"uri"` URI string `json:"uri"`
URL string `json:"url"` URL string `json:"url"`

@ -60,6 +60,7 @@ func (b *Builder) buildSelect() {
b.sql.WriteString( b.sql.WriteString(
`SELECT `SELECT
dashboard.id, dashboard.id,
dashboard.org_id,
dashboard.uid, dashboard.uid,
dashboard.title, dashboard.title,
dashboard.slug, dashboard.slug,

@ -68,6 +68,7 @@ func TestBuilder_EqualResults_Basic(t *testing.T) {
{ {
ID: dashIds[0], ID: dashIds[0],
Title: "A", Title: "A",
OrgID: 1,
Slug: "a", Slug: "a",
Term: "templated", Term: "templated",
}, },

@ -16413,6 +16413,10 @@
"isStarred": { "isStarred": {
"type": "boolean" "type": "boolean"
}, },
"orgId": {
"type": "integer",
"format": "int64"
},
"permanentlyDeleteDate": { "permanentlyDeleteDate": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"

@ -6487,6 +6487,10 @@
"isStarred": { "isStarred": {
"type": "boolean" "type": "boolean"
}, },
"orgId": {
"format": "int64",
"type": "integer"
},
"permanentlyDeleteDate": { "permanentlyDeleteDate": {
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"

Loading…
Cancel
Save