K8s: Dashboards: Reduce db calls to get users on list (#103020)

pull/103030/head
Stephanie Hingtgen 3 months ago committed by GitHub
parent e6480a050c
commit 12c5b81850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pkg/registry/apis/dashboard/legacy/query_dashboards.sql
  2. 2
      pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-export_with_history.sql
  3. 2
      pkg/registry/apis/dashboard/legacy/testdata/mysql--query_dashboards-history_uid_at_version.sql
  4. 2
      pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-export_with_history.sql
  5. 2
      pkg/registry/apis/dashboard/legacy/testdata/postgres--query_dashboards-history_uid_at_version.sql
  6. 2
      pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-export_with_history.sql
  7. 2
      pkg/registry/apis/dashboard/legacy/testdata/sqlite--query_dashboards-history_uid_at_version.sql
  8. 60
      pkg/services/apiserver/client/client.go
  9. 6
      pkg/services/apiserver/client/client_mock.go
  10. 35
      pkg/services/apiserver/client/client_test.go
  11. 60
      pkg/services/dashboards/service/dashboard_service.go
  12. 32
      pkg/services/dashboards/service/dashboard_service_test.go
  13. 83
      pkg/services/dashboardversion/dashverimpl/dashver.go
  14. 6
      pkg/services/dashboardversion/dashverimpl/dashver_test.go

@ -22,7 +22,7 @@ LEFT OUTER JOIN {{ .Ident .VersionTable }} as dashboard_version ON dashboard.id
{{ end }}
LEFT OUTER JOIN {{ .Ident .ProvisioningTable }} as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN {{ .Ident .UserTable }} as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN {{ .Ident .UserTable }} as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN {{ .Ident .UserTable }} as updated_user ON {{ if .Query.UseHistoryTable }}dashboard_version.created_by = updated_user.id{{ else }}dashboard.updated_by = updated_user.id{{ end }}
WHERE dashboard.is_folder = {{ .Arg .Query.GetFolders }}
AND dashboard.org_id = {{ .Arg .Query.OrgID }}
{{ if .Query.UseHistoryTable }}

@ -14,7 +14,7 @@ FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard_version.created_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 1
ORDER BY

@ -14,7 +14,7 @@ FROM `grafana`.`dashboard` as dashboard
LEFT OUTER JOIN `grafana`.`dashboard_version` as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN `grafana`.`dashboard_provisioning` as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN `grafana`.`user` as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN `grafana`.`user` as updated_user ON dashboard_version.created_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

@ -14,7 +14,7 @@ FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 1
ORDER BY

@ -14,7 +14,7 @@ FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

@ -14,7 +14,7 @@ FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 1
ORDER BY

@ -14,7 +14,7 @@ FROM "grafana"."dashboard" as dashboard
LEFT OUTER JOIN "grafana"."dashboard_version" as dashboard_version ON dashboard.id = dashboard_version.dashboard_id
LEFT OUTER JOIN "grafana"."dashboard_provisioning" as provisioning ON dashboard.id = provisioning.dashboard_id
LEFT OUTER JOIN "grafana"."user" as created_user ON dashboard.created_by = created_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard.updated_by = updated_user.id
LEFT OUTER JOIN "grafana"."user" as updated_user ON dashboard_version.created_by = updated_user.id
WHERE dashboard.is_folder = FALSE
AND dashboard.org_id = 2
AND dashboard.uid = 'UUU'

@ -2,7 +2,6 @@ package client
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
@ -37,7 +36,7 @@ type K8sHandler interface {
List(ctx context.Context, orgID int64, options v1.ListOptions) (*unstructured.UnstructuredList, error)
Search(ctx context.Context, orgID int64, in *resource.ResourceSearchRequest) (*resource.ResourceSearchResponse, error)
GetStats(ctx context.Context, orgID int64) (*resource.ResourceStatsResponse, error)
GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error)
GetUsersFromMeta(ctx context.Context, userMeta []string) (map[string]*user.User, error)
}
var _ K8sHandler = (*k8sHandler)(nil)
@ -203,26 +202,51 @@ func (h *k8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.Resou
})
}
// GetUserFromMeta takes what meta accessor gives you from `GetCreatedBy` or `GetUpdatedBy` and returns the user
func (h *k8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) {
parts := strings.Split(userMeta, ":")
if len(parts) < 2 {
return &user.User{}, nil
// GetUsersFromMeta takes what meta accessor gives you from `GetCreatedBy` or `GetUpdatedBy` and returns the user(s), with the meta as the key
func (h *k8sHandler) GetUsersFromMeta(ctx context.Context, usersMeta []string) (map[string]*user.User, error) {
uids := []string{}
ids := []int64{}
metaToId := make(map[string]int64)
metaToUid := make(map[string]string)
userMap := make(map[string]*user.User)
for _, userMeta := range usersMeta {
parts := strings.Split(userMeta, ":")
if len(parts) < 2 {
return userMap, nil
}
meta := parts[1]
userId, err := strconv.ParseInt(meta, 10, 64)
if err == nil {
ids = append(ids, userId)
metaToId[userMeta] = userId
} else {
uids = append(uids, meta)
metaToUid[userMeta] = meta
}
}
meta := parts[1]
userId, err := strconv.ParseInt(meta, 10, 64)
var u *user.User
if err == nil {
u, err = h.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId})
} else {
u, err = h.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: meta})
users, err := h.userService.ListByIdOrUID(ctx, uids, ids)
if err != nil {
return userMap, nil
}
if err != nil && errors.Is(err, user.ErrUserNotFound) {
return &user.User{}, nil
for _, u := range users {
for meta, id := range metaToId {
if u.ID == id {
userMap[meta] = u
break
}
}
for meta, uid := range metaToUid {
if u.UID == uid {
userMap[meta] = u
break
}
}
}
return u, err
return userMap, err
}
func (h *k8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, error) {

@ -82,12 +82,12 @@ func (m *MockK8sHandler) GetStats(ctx context.Context, orgID int64) (*resource.R
return args.Get(0).(*resource.ResourceStatsResponse), args.Error(1)
}
func (m *MockK8sHandler) GetUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) {
args := m.Called(ctx, userMeta)
func (m *MockK8sHandler) GetUsersFromMeta(ctx context.Context, usersMeta []string) (map[string]*user.User, error) {
args := m.Called(ctx, usersMeta)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*user.User), args.Error(1)
return args.Get(0).(map[string]*user.User), args.Error(1)
}
type MockTestRestConfig struct {

@ -9,26 +9,41 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetUserFromMeta(t *testing.T) {
func TestGetUsersFromMeta(t *testing.T) {
userSvcTest := usertest.NewUserServiceFake()
userSvcTest.ExpectedUser = &user.User{
ID: 1,
UID: "uid-value",
userSvcTest.ExpectedListUsersByIdOrUid = []*user.User{
{
ID: 1,
UID: "uid-value",
},
{
ID: 2,
UID: "uid-value2",
},
}
client := &k8sHandler{
userService: userSvcTest,
}
t.Run("returns user with valid UID", func(t *testing.T) {
result, err := client.GetUserFromMeta(context.Background(), "user:uid-value")
result, err := client.GetUsersFromMeta(context.Background(), []string{"user:uid-value"})
require.NoError(t, err)
require.Equal(t, "uid-value", result.UID)
require.Equal(t, int64(1), result.ID)
require.Equal(t, "uid-value", result["user:uid-value"].UID)
require.Equal(t, int64(1), result["user:uid-value"].ID)
})
t.Run("returns user when id is passed in", func(t *testing.T) {
result, err := client.GetUserFromMeta(context.Background(), "user:1")
result, err := client.GetUsersFromMeta(context.Background(), []string{"user:1"})
require.NoError(t, err)
require.Equal(t, "uid-value", result.UID)
require.Equal(t, int64(1), result.ID)
require.Equal(t, "uid-value", result["user:1"].UID)
require.Equal(t, int64(1), result["user:1"].ID)
})
t.Run("returns users when id and uid are passed in", func(t *testing.T) {
result, err := client.GetUsersFromMeta(context.Background(), []string{"user:1", "user:uid-value2"})
require.NoError(t, err)
require.Equal(t, "uid-value", result["user:1"].UID)
require.Equal(t, int64(1), result["user:1"].ID)
require.Equal(t, "uid-value2", result["user:uid-value2"].UID)
require.Equal(t, int64(2), result["user:uid-value2"].ID)
})
}

@ -320,8 +320,14 @@ func (dr *DashboardServiceImpl) processDashboardBatch(ctx context.Context, orgID
var errs []error
itemsProcessed := 0
// get users ahead of time to do just one db call, rather than 2 per item in the list
users, err := dr.getUsersForList(ctx, items, orgID)
if err != nil {
return 0, append(errs, err)
}
for _, item := range items {
dash, err := dr.UnstructuredToLegacyDashboard(ctx, &item, orgID)
dash, err := dr.unstructuredToLegacyDashboardWithUsers(&item, orgID, users)
if err != nil {
errs = append(errs, fmt.Errorf("failed to convert dashboard: %w", err))
continue
@ -1924,9 +1930,15 @@ func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, or
return nil, dashboards.ErrDashboardNotFound
}
// get users ahead of time to do just one db call, rather than 2 per item in the list
users, err := dr.getUsersForList(ctx, out.Items, orgID)
if err != nil {
return nil, err
}
dashboards := make([]*dashboards.Dashboard, 0)
for _, item := range out.Items {
dash, err := dr.UnstructuredToLegacyDashboard(ctx, &item, orgID)
dash, err := dr.unstructuredToLegacyDashboardWithUsers(&item, orgID, users)
if err != nil {
return nil, err
}
@ -2204,7 +2216,38 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8s(ctx context.Context,
return result, nil
}
func (dr *DashboardServiceImpl) getUsersForList(ctx context.Context, items []unstructured.Unstructured, orgID int64) (map[string]*user.User, error) {
userMeta := []string{}
for _, item := range items {
obj, err := utils.MetaAccessor(&item)
if err != nil {
return nil, err
}
if obj.GetCreatedBy() != "" {
userMeta = append(userMeta, obj.GetCreatedBy())
}
if obj.GetUpdatedBy() != "" {
userMeta = append(userMeta, obj.GetUpdatedBy())
}
}
return dr.k8sclient.GetUsersFromMeta(ctx, userMeta)
}
func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) {
obj, err := utils.MetaAccessor(item)
if err != nil {
return nil, err
}
users, err := dr.k8sclient.GetUsersFromMeta(ctx, []string{obj.GetCreatedBy(), obj.GetUpdatedBy()})
if err != nil {
return nil, err
}
return dr.unstructuredToLegacyDashboardWithUsers(item, orgID, users)
}
func (dr *DashboardServiceImpl) unstructuredToLegacyDashboardWithUsers(item *unstructured.Unstructured, orgID int64, users map[string]*user.User) (*dashboards.Dashboard, error) {
spec, ok := item.Object["spec"].(map[string]any)
if !ok {
return nil, errors.New("error parsing dashboard from k8s response")
@ -2247,17 +2290,12 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
out.PluginID = dashboard.GetPluginIDFromMeta(obj)
creator, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy())
if err != nil {
return nil, err
if creator, ok := users[obj.GetCreatedBy()]; ok {
out.CreatedBy = creator.ID
}
out.CreatedBy = creator.ID
updater, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetUpdatedBy())
if err != nil {
return nil, err
if updater, ok := users[obj.GetUpdatedBy()]; ok {
out.UpdatedBy = updater.ID
}
out.UpdatedBy = updater.ID
// any dashboards that have already been synced to unified storage will have the id in the spec
// and not as a label. We will need to support this conversion until they have all been updated

@ -320,7 +320,7 @@ func TestGetDashboard(t *testing.T) {
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
}
k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once()
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
dashboard, err := service.GetDashboard(ctx, query)
require.NoError(t, err)
@ -358,7 +358,7 @@ func TestGetDashboard(t *testing.T) {
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(2)}),
}
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once()
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@ -457,7 +457,7 @@ func TestGetAllDashboards(t *testing.T) {
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
}
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()
dashes, err := service.GetAllDashboards(ctx)
@ -509,7 +509,7 @@ func TestGetAllDashboardsByOrgId(t *testing.T) {
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
}
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()
dashes, err := service.GetAllDashboardsByOrgId(ctx, 1)
@ -1140,7 +1140,7 @@ func TestUnprovisionDashboard(t *testing.T) {
// should update it to be without annotations
k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything).Return(dashWithoutAnnotations, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@ -1207,7 +1207,7 @@ func TestGetDashboardsByPluginID(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(uidUnstructured, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
return ( // gofmt comment helper
req.Options.Fields[0].Key == "manager.kind" && req.Options.Fields[0].Values[0] == string(utils.ManagerKindPlugin) &&
@ -1361,7 +1361,7 @@ func TestSaveProvisionedDashboard(t *testing.T) {
t.Run("Should use Kubernetes create if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
fakeStore.On("SaveProvisionedDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
@ -1422,7 +1422,7 @@ func TestSaveDashboard(t *testing.T) {
t.Run("Should use Kubernetes create if feature flags are enabled and dashboard doesn't exist", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
@ -1433,7 +1433,7 @@ func TestSaveDashboard(t *testing.T) {
t.Run("Should use Kubernetes update if feature flags are enabled and dashboard exists", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
@ -1859,7 +1859,7 @@ func TestGetDashboards(t *testing.T) {
k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default")
k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything, mock.Anything).Return(uid1Unstructured, nil)
k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(uid2Unstructured, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
k8sCliMock.On("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@ -1977,7 +1977,7 @@ func TestGetDashboardUIDByID(t *testing.T) {
func TestUnstructuredToLegacyDashboard(t *testing.T) {
k8sCliMock := new(client.MockK8sHandler)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{ID: 10, UID: "useruid"}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{"user:useruid": &user.User{ID: 10, UID: "useruid"}}, nil)
dr := &DashboardServiceImpl{
k8sclient: k8sCliMock,
}
@ -2022,7 +2022,7 @@ func TestUnstructuredToLegacyDashboard(t *testing.T) {
item := &unstructured.Unstructured{
Object: map[string]interface{}{},
}
_, err := (&DashboardServiceImpl{}).UnstructuredToLegacyDashboard(context.Background(), item, int64(123))
_, err := dr.UnstructuredToLegacyDashboard(context.Background(), item, int64(123))
assert.Error(t, err)
assert.Equal(t, "error parsing dashboard from k8s response", err.Error())
})
@ -2659,8 +2659,8 @@ func TestK8sDashboardCleanupJob(t *testing.T) {
Items: []unstructured.Unstructured{dashboard2},
}, nil).Once()
// Mock GetUserFromMeta calls
k8sCliMock.On("GetUserFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(&user.User{}, nil).Times(4)
// should be called twice, one for each list call
k8sCliMock.On("GetUsersFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(map[string]*user.User{}, nil).Times(2)
// Mock cleanup
fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash1"}).Return(nil).Once()
@ -2737,8 +2737,8 @@ func TestK8sDashboardCleanupJob(t *testing.T) {
Items: secondBatch,
}, nil).Once()
// Mock GetUserFromMeta calls for each dashboard
k8sCliMock.On("GetUserFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(&user.User{}, nil).Times(10)
// should be called twice, one for each list call
k8sCliMock.On("GetUsersFromMeta", mock.AnythingOfType("*context.valueCtx"), mock.Anything).Return(map[string]*user.User{}, nil).Times(2)
// Mock public dashboard deletion for each dashboard
fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, int64(1), []string{"dash1"}).Return(nil).Once()

@ -276,13 +276,9 @@ func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashbo
return nil, dashboards.ErrDashboardNotFound
}
dashboards := make([]*dashver.DashboardVersionDTO, len(out.Items))
for i, item := range out.Items {
dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, &item, orgID)
if err != nil {
return nil, err
}
dashboards[i] = dash
dashboards, err := s.UnstructuredToLegacyDashboardVersionList(ctx, out.Items, orgID)
if err != nil {
return nil, err
}
return &dashver.DashboardVersionResponse{
@ -292,6 +288,51 @@ func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashbo
}
func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashver.DashboardVersionDTO, error) {
obj, err := utils.MetaAccessor(item)
if err != nil {
return nil, err
}
users, err := s.k8sclient.GetUsersFromMeta(ctx, []string{obj.GetCreatedBy(), obj.GetUpdatedBy()})
if err != nil {
return nil, err
}
return s.unstructuredToLegacyDashboardVersionWithUsers(item, users)
}
func (s *Service) UnstructuredToLegacyDashboardVersionList(ctx context.Context, items []unstructured.Unstructured, orgID int64) ([]*dashver.DashboardVersionDTO, error) {
// get users ahead of time to do just one db call, rather than 2 per item in the list
userMeta := []string{}
for _, item := range items {
obj, err := utils.MetaAccessor(&item)
if err != nil {
return nil, err
}
if obj.GetCreatedBy() != "" {
userMeta = append(userMeta, obj.GetCreatedBy())
}
if obj.GetUpdatedBy() != "" {
userMeta = append(userMeta, obj.GetUpdatedBy())
}
}
users, err := s.k8sclient.GetUsersFromMeta(ctx, userMeta)
if err != nil {
return nil, err
}
versions := make([]*dashver.DashboardVersionDTO, len(items))
for i, item := range items {
version, err := s.unstructuredToLegacyDashboardVersionWithUsers(&item, users)
if err != nil {
return nil, err
}
versions[i] = version
}
return versions, nil
}
func (s *Service) unstructuredToLegacyDashboardVersionWithUsers(item *unstructured.Unstructured, users map[string]*user.User) (*dashver.DashboardVersionDTO, error) {
spec, ok := item.Object["spec"].(map[string]any)
if !ok {
return nil, errors.New("error parsing dashboard from k8s response")
@ -312,19 +353,21 @@ func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item
spec["version"] = dashVersion
}
createdBy, err := s.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy())
if err != nil {
return nil, err
var createdBy *user.User
if creator, ok := users[obj.GetCreatedBy()]; ok {
createdBy = creator
}
// if updated by is set, then this version of the dashboard was "created"
// by that user
if obj.GetUpdatedBy() != "" {
updatedBy, err := s.k8sclient.GetUserFromMeta(ctx, obj.GetUpdatedBy())
if err == nil && updatedBy != nil {
createdBy = updatedBy
}
if updater, ok := users[obj.GetUpdatedBy()]; ok {
createdBy = updater
}
createdByID := int64(0)
if createdBy != nil {
createdByID = createdBy.ID
}
created := obj.GetCreationTimestamp().Time
if updated, err := obj.GetUpdatedTimestamp(); err == nil && updated != nil {
created = *updated
@ -335,20 +378,18 @@ func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item
return nil, err
}
out := dashver.DashboardVersionDTO{
return &dashver.DashboardVersionDTO{
ID: dashVersion,
DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
DashboardUID: uid,
Created: created,
CreatedBy: createdBy.ID,
CreatedBy: createdByID,
Message: obj.GetMessage(),
RestoredFrom: restoreVer,
Version: int(dashVersion),
ParentVersion: int(parentVersion),
Data: simplejson.NewFromAny(spec),
}
return &out, nil
}, nil
}
var restoreMsg = "Restored from version "

@ -73,7 +73,7 @@ func TestDashboardVersionService(t *testing.T) {
obj, err := utils.MetaAccessor(dash)
require.NoError(t, err)
obj.SetUpdatedTimestamp(&updatedTimestamp)
mockCli.On("GetUserFromMeta", mock.Anything, "user:1").Return(&user.User{ID: 1}, nil)
mockCli.On("GetUsersFromMeta", mock.Anything, []string{"user:1", ""}).Return(map[string]*user.User{"user:1": &user.User{ID: 1}}, nil)
mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(&unstructured.UnstructuredList{
Items: []unstructured.Unstructured{*dash}}, nil).Once()
res, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{
@ -93,7 +93,7 @@ func TestDashboardVersionService(t *testing.T) {
Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(10), "hello": "world"}),
})
mockCli.On("GetUserFromMeta", mock.Anything, "user:2").Return(&user.User{ID: 2}, nil)
mockCli.On("GetUsersFromMeta", mock.Anything, []string{"user:1", "user:2"}).Return(map[string]*user.User{"user:1": {ID: 1}, "user:2": {ID: 2}}, nil)
mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(&unstructured.UnstructuredList{
Items: []unstructured.Unstructured{{
Object: map[string]any{
@ -269,7 +269,7 @@ func TestListDashboardVersions(t *testing.T) {
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
query := dashver.ListDashboardVersionsQuery{DashboardID: 42}
mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
mockCli.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{}, nil)
mockCli.On("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{
Items: []unstructured.Unstructured{{Object: map[string]any{
"metadata": map[string]any{

Loading…
Cancel
Save