From 01b3e56706932fecc18c0d4551de01ea6c16aab5 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Thu, 2 Jan 2025 10:38:23 -0700 Subject: [PATCH] K8s Dashboards: Fix Tags (#98417) --- pkg/services/dashboards/models.go | 1 + .../dashboards/service/dashboard_service.go | 71 ++++++++++++++++--- .../service/dashboard_service_test.go | 68 +++++++++++++++++- 3 files changed, 128 insertions(+), 12 deletions(-) diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index b27b5f45cd4..be1d8032407 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -316,6 +316,7 @@ type DashboardSearchProjection struct { FolderSlug string FolderTitle string SortMeta int64 + Tags []string Deleted *time.Time } diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 75367528081..e0f7dfd9f18 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -839,20 +839,21 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb query.OrgId = requester.GetOrgID() } - results, err := dr.searchDashboardsThroughK8s(ctx, query) + response, err := dr.searchDashboardsThroughK8sRaw(ctx, query) if err != nil { return nil, err } - finalResults := make([]dashboards.DashboardSearchProjection, len(results)) - for i, result := range results { + finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits)) + for i, hit := range response.Hits { finalResults[i] = dashboards.DashboardSearchProjection{ - UID: result.UID, - OrgID: result.OrgID, - Title: result.Title, - Slug: result.Slug, + UID: hit.Name, + OrgID: query.OrgId, + Title: hit.Title, + Slug: slugify.Slugify(hit.Title), IsFolder: false, - FolderUID: result.FolderUID, + FolderUID: hit.Folder, + Tags: hit.Tags, } } @@ -928,6 +929,13 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb Tags: []string{}, } + // when searching through unified storage, the dashboard will come as one + // item, when searching through legacy, the dashboard will come multiple times + // per tag. So we need to add the array here for unified, and the term below for legacy. + if item.Tags != nil { + hit.Tags = item.Tags + } + // nolint:staticcheck if item.FolderID > 0 { hit.FolderURL = dashboards.GetFolderURL(item.FolderUID, item.FolderSlug) @@ -954,7 +962,41 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb } func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) { - // TODO: use k8s client to get dashboards first, and then filter tags and join + if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesCliDashboards) { + res, err := dr.k8sclient.getSearcher().Search(ctx, &resource.ResourceSearchRequest{ + Options: &resource.ListOptions{ + Key: &resource.ResourceKey{ + Namespace: dr.k8sclient.getNamespace(query.OrgID), + Group: "dashboard.grafana.app", + Resource: "dashboards", + }, + }, + Facet: map[string]*resource.ResourceSearchRequest_Facet{ + "tags": { + Field: "tags", + Limit: 100000, + }, + }, + Limit: 100000}) + if err != nil { + return nil, err + } + facet, ok := res.Facet["tags"] + if !ok { + return []*dashboards.DashboardTagCloudItem{}, nil + } + + results := make([]*dashboards.DashboardTagCloudItem, len(facet.Terms)) + for i, item := range facet.Terms { + results[i] = &dashboards.DashboardTagCloudItem{ + Term: item.Term, + Count: int(item.Count), + } + } + + return results, nil + } + return dr.dashboardStore.GetDashboardTags(ctx, query) } @@ -1206,7 +1248,7 @@ func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, or return dashboards, nil } -func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) { +func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (*v0alpha1.SearchResults, error) { dashboardskey := &resource.ResourceKey{ Namespace: dr.k8sclient.getNamespace(query.OrgId), Group: "dashboard.grafana.app", @@ -1287,7 +1329,14 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, return nil, err } - response := ParseResults(res, 0) + return ParseResults(res, 0), nil +} + +func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]*dashboards.Dashboard, error) { + response, err := dr.searchDashboardsThroughK8sRaw(ctx, query) + if err != nil { + return nil, err + } result := make([]*dashboards.Dashboard, len(response.Hits)) for i, hit := range response.Hits { result[i] = &dashboards.Dashboard{ diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 7427fa4d1b9..6e9c2f9ec4d 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -660,7 +660,10 @@ func TestSearchDashboards(t *testing.T) { Type: "dash-db", URI: "db/dashboard-1", URL: "/d/uid1/dashboard-1", - Tags: []string{}, + Tags: []string{ + "tag1", + "tag2", + }, }, { UID: "uid2", @@ -683,6 +686,7 @@ func TestSearchDashboards(t *testing.T) { Slug: "dashboard-1", OrgID: 1, Title: "Dashboard 1", + Tags: []string{"tag1", "tag2"}, }, { UID: "uid2", @@ -709,6 +713,9 @@ func TestSearchDashboards(t *testing.T) { { Name: "folder", }, + { + Name: "tags", + }, }, Rows: []*resource.ResourceTableRow{ { @@ -719,6 +726,7 @@ func TestSearchDashboards(t *testing.T) { Cells: [][]byte{ []byte("Dashboard 1"), []byte(""), + []byte("[\"tag1\", \"tag2\"]"), }, }, { @@ -729,6 +737,7 @@ func TestSearchDashboards(t *testing.T) { Cells: [][]byte{ []byte("Dashboard 2"), []byte(""), + []byte(""), }, }, }, @@ -958,6 +967,63 @@ func TestUnstructuredToLegacyDashboard(t *testing.T) { }) } +func TestGetDashboardTags(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + defer fakeStore.AssertExpectations(t) + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + dashboardStore: &fakeStore, + } + + expectedResult := []*dashboards.DashboardTagCloudItem{ + { + Term: "tag1", + Count: 1, + }, + { + Term: "tag2", + Count: 3, + }, + } + query := &dashboards.GetDashboardTagsQuery{ + OrgID: 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("GetDashboardTags", mock.Anything, query).Return(expectedResult, nil).Once() + result, err := service.GetDashboardTags(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{ + Facet: map[string]*resource.ResourceSearchResponse_Facet{ + "tags": { + Terms: []*resource.ResourceSearchResponse_TermFacet{ + { + Term: "tag1", + Count: 1, + }, + { + Term: "tag2", + Count: 3, + }, + }, + }, + }, + }, nil) + + result, err := service.GetDashboardTags(ctx, query) + require.NoError(t, err) + require.Equal(t, expectedResult, result) + fakeStore.AssertExpectations(t) + }) +} + func TestLegacySaveCommandToUnstructured(t *testing.T) { namespace := "test-namespace" t.Run("successfully converts save command to unstructured", func(t *testing.T) {