Dashboard Versions: Make compatible with app platform (#99327)

This commit is contained in:
Stephanie Hingtgen
2025-01-28 08:17:52 -06:00
committed by GitHub
parent 05905a5069
commit 0cef2b9ae7
44 changed files with 887 additions and 413 deletions
+33 -4
View File
@@ -2,7 +2,10 @@ package client
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -16,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
k8sUser "k8s.io/apiserver/pkg/authentication/user"
@@ -24,7 +28,7 @@ import (
type K8sHandler interface {
GetNamespace(orgID int64) string
Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error)
Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error)
Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error)
Update(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error)
Delete(ctx context.Context, name string, orgID int64, options v1.DeleteOptions) error
@@ -32,6 +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)
}
var _ K8sHandler = (*k8sHandler)(nil)
@@ -41,9 +46,10 @@ type k8sHandler struct {
gvr schema.GroupVersionResource
restConfigProvider apiserver.RestConfigProvider
searcher resource.ResourceIndexClient
userService user.Service
}
func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store) K8sHandler {
func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr schema.GroupVersionResource, restConfigProvider apiserver.RestConfigProvider, searcher resource.ResourceIndexClient, dashStore dashboards.Store, userSvc user.Service) K8sHandler {
legacySearcher := legacysearcher.NewDashboardSearchClient(dashStore)
searchClient := resource.NewSearchClient(cfg, setting.UnifiedStorageConfigKeyDashboard, searcher, legacySearcher)
return &k8sHandler{
@@ -51,6 +57,7 @@ func NewK8sHandler(cfg *setting.Cfg, namespacer request.NamespaceMapper, gvr sch
gvr: gvr,
restConfigProvider: restConfigProvider,
searcher: searchClient,
userService: userSvc,
}
}
@@ -58,7 +65,7 @@ func (h *k8sHandler) GetNamespace(orgID int64) string {
return h.namespacer(orgID)
}
func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) {
func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, 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 := h.getK8sContext(ctx)
@@ -73,7 +80,7 @@ func (h *k8sHandler) Get(ctx context.Context, name string, orgID int64, subresou
return nil, nil
}
return client.Get(newCtx, name, v1.GetOptions{}, subresource...)
return client.Get(newCtx, name, options, subresource...)
}
func (h *k8sHandler) Create(ctx context.Context, obj *unstructured.Unstructured, orgID int64) (*unstructured.Unstructured, error) {
@@ -191,6 +198,28 @@ 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
}
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})
}
if err != nil && errors.Is(err, user.ErrUserNotFound) {
return &user.User{}, nil
}
return u, err
}
func (h *k8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
cfg := h.restConfigProvider.GetRestConfig(ctx)
if cfg == nil {
+11 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -21,8 +22,8 @@ func (m *MockK8sHandler) GetNamespace(orgID int64) string {
return args.String(0)
}
func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, subresource ...string) (*unstructured.Unstructured, error) {
args := m.Called(ctx, name, orgID, subresource)
func (m *MockK8sHandler) Get(ctx context.Context, name string, orgID int64, options v1.GetOptions, subresource ...string) (*unstructured.Unstructured, error) {
args := m.Called(ctx, name, orgID, options, subresource)
if args.Get(0) == nil {
return nil, args.Error(1)
}
@@ -79,3 +80,11 @@ 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)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*user.User), args.Error(1)
}
@@ -0,0 +1,34 @@
package client
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/stretchr/testify/require"
)
func TestGetUserFromMeta(t *testing.T) {
userSvcTest := usertest.NewUserServiceFake()
userSvcTest.ExpectedUser = &user.User{
ID: 1,
UID: "uid-value",
}
client := &k8sHandler{
userService: userSvcTest,
}
t.Run("returns user with valid UID", func(t *testing.T) {
result, err := client.GetUserFromMeta(context.Background(), "user:uid-value")
require.NoError(t, err)
require.Equal(t, "uid-value", result.UID)
require.Equal(t, int64(1), result.ID)
})
t.Run("returns user when id is passed in", func(t *testing.T) {
result, err := client.GetUserFromMeta(context.Background(), "user:1")
require.NoError(t, err)
require.Equal(t, "uid-value", result.UID)
require.Equal(t, int64(1), result.ID)
})
}
@@ -68,7 +68,6 @@ type DashboardServiceImpl struct {
dashboardStore dashboards.Store
folderStore folder.FolderStore
folderService folder.Service
userService user.Service
orgService org.Service
features featuremgmt.FeatureToggles
folderPermissions accesscontrol.FolderPermissionsService
@@ -91,7 +90,7 @@ func ProvideDashboardServiceImpl(
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient,
quotaService quota.Service, orgService org.Service, publicDashboardService publicdashboards.ServiceWrapper,
) (*DashboardServiceImpl, error) {
k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore)
k8sHandler := client.NewK8sHandler(cfg, request.GetNamespaceMapper(cfg), v0alpha1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider, unified, dashboardStore, userService)
dashSvc := &DashboardServiceImpl{
cfg: cfg,
@@ -103,7 +102,6 @@ func ProvideDashboardServiceImpl(
folderStore: folderStore,
folderService: folderSvc,
orgService: orgService,
userService: userService,
k8sclient: k8sHandler,
metrics: newDashboardsMetrics(r),
dashboardPermissionsReady: make(chan struct{}),
@@ -1489,7 +1487,7 @@ func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, quer
query.UID = result.UID
}
out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, subresource)
out, err := dr.k8sclient.Get(ctx, query.UID, query.OrgID, v1.GetOptions{}, subresource)
if err != nil && !apierrors.IsNotFound(err) {
return nil, err
} else if err != nil || out == nil {
@@ -1553,7 +1551,7 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd
func (dr *DashboardServiceImpl) createOrUpdateDash(ctx context.Context, obj unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) {
var out *unstructured.Unstructured
current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID)
current, err := dr.k8sclient.Get(ctx, obj.GetName(), orgID, v1.GetOptions{})
if current == nil || err != nil {
out, err = dr.k8sclient.Create(ctx, &obj, orgID)
if err != nil {
@@ -1751,7 +1749,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex
for _, h := range searchResults.Hits {
func(hit v0alpha1.DashboardHit) {
g.Go(func() error {
out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId)
out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId, v1.GetOptions{})
if err != nil {
return err
} else if out == nil {
@@ -1863,13 +1861,13 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
out.PluginID = GetPluginIDFromMeta(obj)
creator, err := dr.getUserFromMeta(ctx, obj.GetCreatedBy())
creator, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy())
if err != nil {
return nil, err
}
out.CreatedBy = creator.ID
updater, err := dr.getUserFromMeta(ctx, obj.GetUpdatedBy())
updater, err := dr.k8sclient.GetUserFromMeta(ctx, obj.GetUpdatedBy())
if err != nil {
return nil, err
}
@@ -1906,25 +1904,6 @@ func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Contex
return &out, nil
}
func (dr *DashboardServiceImpl) getUserFromMeta(ctx context.Context, userMeta string) (*user.User, error) {
if userMeta == "" || toUID(userMeta) == "" {
return &user.User{}, nil
}
usr, err := dr.getUser(ctx, toUID(userMeta))
if err != nil && errors.Is(err, user.ErrUserNotFound) {
return &user.User{}, nil
}
return usr, err
}
func (dr *DashboardServiceImpl) getUser(ctx context.Context, uid string) (*user.User, error) {
userId, err := strconv.ParseInt(uid, 10, 64)
if err == nil {
return dr.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: userId})
}
return dr.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: uid})
}
var pluginIDRepoName = "plugin"
var fileProvisionedRepoPrefix = "file:"
@@ -2010,11 +1989,3 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names
return finalObj, nil
}
func toUID(rawIdentifier string) string {
parts := strings.Split(rawIdentifier, ":")
if len(parts) < 2 {
return ""
}
return parts[1]
}
@@ -26,7 +26,6 @@ import (
"github.com/grafana/grafana/pkg/services/quota"
"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"
@@ -315,7 +314,8 @@ func TestGetDashboard(t *testing.T) {
Version: 1,
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).Return(&dashboardUnstructured, nil).Once()
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)
dashboard, err := service.GetDashboard(ctx, query)
require.NoError(t, err)
@@ -351,7 +351,8 @@ func TestGetDashboard(t *testing.T) {
Version: 1,
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid", "version": int64(1)}),
}
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once()
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("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -391,7 +392,7 @@ func TestGetDashboard(t *testing.T) {
t.Run("Should return error when Kubernetes client fails", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
dashboard, err := service.GetDashboard(ctx, query)
require.Error(t, err)
@@ -401,7 +402,7 @@ func TestGetDashboard(t *testing.T) {
t.Run("Should return dashboard not found if Kubernetes client returns nil", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, nil).Once()
k8sCliMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once()
dashboard, err := service.GetDashboard(ctx, query)
require.Error(t, err)
require.Equal(t, dashboards.ErrDashboardNotFound, err)
@@ -450,6 +451,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("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()
dashes, err := service.GetAllDashboards(ctx)
@@ -501,6 +503,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("List", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()
dashes, err := service.GetAllDashboardsByOrgId(ctx, 1)
@@ -534,7 +537,7 @@ func TestGetProvisionedDashboardData(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled and get from relevant org", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"labels": map[string]any{
@@ -632,7 +635,7 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled and get from whatever org it is in", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"labels": map[string]any{
@@ -721,7 +724,7 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"labels": map[string]any{
@@ -812,7 +815,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid", OrgID: 1}).Return(nil).Once()
fakeStore.On("CleanupAfterDelete", mock.Anything, &dashboards.DeleteDashboardCommand{UID: "uid3", OrgID: 2}).Return(nil).Once()
fakePublicDashboardService.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"annotations": map[string]any{
@@ -825,7 +828,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
"spec": map[string]any{},
}}, nil).Once()
// should not delete this one, because it does not start with "file:"
k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid2",
"annotations": map[string]any{
@@ -836,7 +839,7 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) {
"spec": map[string]any{},
}}, nil).Once()
k8sCliMock.On("Get", mock.Anything, "uid3", mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
k8sCliMock.On("Get", mock.Anything, "uid3", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]any{
"metadata": map[string]any{
"name": "uid3",
"annotations": map[string]any{
@@ -958,7 +961,7 @@ func TestUnprovisionDashboard(t *testing.T) {
},
"spec": map[string]any{},
}}
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil)
dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "dashboard.grafana.app/v0alpha1",
"kind": "Dashboard",
@@ -975,6 +978,7 @@ func TestUnprovisionDashboard(t *testing.T) {
// should update it to be without annotations
k8sCliMock.On("Update", mock.Anything, dashWithoutAnnotations, mock.Anything, 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("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -1039,7 +1043,8 @@ func TestGetDashboardsByPluginID(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, "uid", mock.Anything, mock.Anything).Return(uidUnstructured, nil)
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("Search", mock.Anything, mock.Anything, mock.MatchedBy(func(req *resource.ResourceSearchRequest) bool {
return req.Options.Fields[0].Key == "repo.name" && req.Options.Fields[0].Values[0] == "plugin" &&
req.Options.Fields[1].Key == "repo.path" && req.Options.Fields[1].Values[0] == "testing"
@@ -1127,7 +1132,8 @@ 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(nil)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
@@ -1188,7 +1194,8 @@ 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("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
@@ -1199,7 +1206,8 @@ 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("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
@@ -1210,7 +1218,7 @@ func TestSaveDashboard(t *testing.T) {
t.Run("Should return an error if uid is invalid", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil)
k8sCliMock.On("GetNamespace", mock.Anything).Return("default")
k8sCliMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
@@ -1495,8 +1503,9 @@ func TestGetDashboards(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sCliMock := setupK8sDashboardTests(service)
k8sCliMock.On("Get", mock.Anything, "uid1", mock.Anything, mock.Anything).Return(uid1Unstructured, nil)
k8sCliMock.On("Get", mock.Anything, "uid2", mock.Anything, mock.Anything).Return(uid2Unstructured, nil)
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("Search", mock.Anything, mock.Anything, mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -1611,10 +1620,10 @@ func TestGetDashboardUIDByID(t *testing.T) {
}
func TestUnstructuredToLegacyDashboard(t *testing.T) {
fake := usertest.NewUserServiceFake()
fake.ExpectedUser = &user.User{ID: 10, UID: "useruid"}
k8sCliMock := new(client.MockK8sHandler)
k8sCliMock.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{ID: 10, UID: "useruid"}, nil)
dr := &DashboardServiceImpl{
userService: fake,
k8sclient: k8sCliMock,
}
t.Run("successfully converts unstructured to legacy dashboard", func(t *testing.T) {
uid := "36b7c825-79cc-435e-acf6-c78bd96a4510"
@@ -1912,17 +1921,3 @@ func TestLegacySaveCommandToUnstructured(t *testing.T) {
assert.Equal(t, result.GetAnnotations(), map[string]string(nil))
})
}
func TestToUID(t *testing.T) {
t.Run("parses valid UID", func(t *testing.T) {
rawIdentifier := "user:uid-value"
result := toUID(rawIdentifier)
assert.Equal(t, "uid-value", result)
})
t.Run("returns empty string for invalid identifier", func(t *testing.T) {
rawIdentifier := "invalid-uid"
result := toUID(rawIdentifier)
assert.Equal(t, "", result)
})
}
+1 -1
View File
@@ -7,5 +7,5 @@ import (
type Service interface {
Get(context.Context, *GetDashboardVersionQuery) (*DashboardVersionDTO, error)
DeleteExpired(context.Context, *DeleteExpiredVersionsCommand) error
List(context.Context, *ListDashboardVersionsQuery) ([]*DashboardVersionDTO, error)
List(context.Context, *ListDashboardVersionsQuery) (*DashboardVersionResponse, error)
}
@@ -3,12 +3,26 @@ package dashverimpl
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
@@ -17,19 +31,32 @@ const (
)
type Service struct {
cfg *setting.Cfg
store store
dashSvc dashboards.DashboardService
log log.Logger
cfg *setting.Cfg
store store
dashSvc dashboards.DashboardService
k8sclient client.K8sHandler
features featuremgmt.FeatureToggles
log log.Logger
}
func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService) dashver.Service {
func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.DashboardService, dashboardStore dashboards.Store, features featuremgmt.FeatureToggles,
restConfigProvider apiserver.RestConfigProvider, userService user.Service, unified resource.ResourceClient) dashver.Service {
return &Service{
cfg: cfg,
store: &sqlStore{
db: db,
dialect: db.GetDialect(),
},
features: features,
k8sclient: client.NewK8sHandler(
cfg,
request.GetNamespaceMapper(cfg),
v0alpha1.DashboardResourceInfo.GroupVersionResource(),
restConfigProvider,
unified,
dashboardStore,
userService,
),
dashSvc: dashboardService,
log: log.New("dashboard-version"),
}
@@ -49,13 +76,21 @@ func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQue
// versions table, at time of this writing), so get the DashboardID if it
// was not populated.
if query.DashboardID == 0 {
id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID)
id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID)
if err != nil {
return nil, err
}
query.DashboardID = id
}
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version)
if err != nil {
return nil, err
}
return version, nil
}
version, err := s.store.Get(ctx, query)
if err != nil {
return nil, err
@@ -95,7 +130,7 @@ func (s *Service) DeleteExpired(ctx context.Context, cmd *dashver.DeleteExpiredV
}
// List all dashboard versions for the given dashboard ID.
func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) ([]*dashver.DashboardVersionDTO, error) {
func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) {
// Get the DashboardUID if not populated
if query.DashboardUID == "" {
u, err := s.getDashUIDMaybeEmpty(ctx, query.DashboardID)
@@ -109,7 +144,7 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions
// versions table, at time of this writing), so get the DashboardID if it
// was not populated.
if query.DashboardID == 0 {
id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID)
id, err := s.getDashIDMaybeEmpty(ctx, query.DashboardUID, query.OrgID)
if err != nil {
return nil, err
}
@@ -118,6 +153,21 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions
if query.Limit == 0 {
query.Limit = 1000
}
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
versions, err := s.listHistoryThroughK8s(
ctx,
query.OrgID,
query.DashboardUID,
int64(query.Limit),
query.ContinueToken,
)
if err != nil {
return nil, err
}
return versions, nil
}
dvs, err := s.store.List(ctx, query)
if err != nil {
return nil, err
@@ -126,7 +176,9 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions
for i, v := range dvs {
dtos[i] = v.ToDTO(query.DashboardUID)
}
return dtos, nil
return &dashver.DashboardVersionResponse{
Versions: dtos,
}, nil
}
// getDashUIDMaybeEmpty is a helper function which takes a dashboardID and
@@ -149,8 +201,8 @@ func (s *Service) getDashUIDMaybeEmpty(ctx context.Context, id int64) (string, e
// getDashIDMaybeEmpty is a helper function which takes a dashboardUID and
// returns the ID. If the dashboard is not found, it will return -1.
func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string) (int64, error) {
q := dashboards.GetDashboardQuery{UID: uid}
func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string, orgID int64) (int64, error) {
q := dashboards.GetDashboardQuery{UID: uid, OrgID: orgID}
result, err := s.dashSvc.GetDashboard(ctx, &q)
if err != nil {
if errors.Is(err, dashboards.ErrDashboardNotFound) {
@@ -163,3 +215,116 @@ func (s *Service) getDashIDMaybeEmpty(ctx context.Context, uid string) (int64, e
}
return result.ID, nil
}
func (s *Service) getHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, rv int64) (*dashver.DashboardVersionDTO, error) {
out, err := s.k8sclient.Get(ctx, dashboardUID, orgID, v1.GetOptions{ResourceVersion: strconv.FormatInt(rv, 10)})
if err != nil {
return nil, err
} else if out == nil {
return nil, dashboards.ErrDashboardNotFound
}
dash, err := s.UnstructuredToLegacyDashboardVersion(ctx, out, orgID)
if err != nil {
return nil, err
}
return dash, nil
}
func (s *Service) listHistoryThroughK8s(ctx context.Context, orgID int64, dashboardUID string, limit int64, continueToken string) (*dashver.DashboardVersionResponse, error) {
out, err := s.k8sclient.List(ctx, orgID, v1.ListOptions{
LabelSelector: utils.LabelKeyGetHistory + "=" + dashboardUID,
Limit: limit,
Continue: continueToken,
})
if err != nil {
return nil, err
} else if out == nil {
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
}
return &dashver.DashboardVersionResponse{
ContinueToken: out.GetContinue(),
Versions: dashboards,
}, nil
}
func (s *Service) UnstructuredToLegacyDashboardVersion(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashver.DashboardVersionDTO, error) {
spec, ok := item.Object["spec"].(map[string]any)
if !ok {
return nil, errors.New("error parsing dashboard from k8s response")
}
obj, err := utils.MetaAccessor(item)
if err != nil {
return nil, err
}
uid := obj.GetName()
spec["uid"] = uid
dashVersion := 0
parentVersion := 0
if version, ok := spec["version"].(int64); ok {
dashVersion = int(version)
parentVersion = dashVersion - 1
}
createdBy, err := s.k8sclient.GetUserFromMeta(ctx, obj.GetCreatedBy())
if err != nil {
return nil, err
}
id, err := obj.GetResourceVersionInt64()
if err != nil {
return nil, err
}
restoreVer, err := getRestoreVersion(obj.GetMessage())
if err != nil {
return nil, err
}
out := dashver.DashboardVersionDTO{
ID: id,
DashboardID: obj.GetDeprecatedInternalID(), // nolint:staticcheck
DashboardUID: uid,
Created: obj.GetCreationTimestamp().Time,
CreatedBy: createdBy.ID,
Message: obj.GetMessage(),
RestoredFrom: restoreVer,
Version: dashVersion,
ParentVersion: parentVersion,
Data: simplejson.NewFromAny(spec),
}
return &out, nil
}
var restoreMsg = "Restored from version "
func DashboardRestoreMessage(version int) string {
return fmt.Sprintf("%s%d", restoreMsg, version)
}
func getRestoreVersion(msg string) (int, error) {
parts := strings.Split(msg, restoreMsg)
if len(parts) < 2 {
return 0, nil
}
ver, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return 0, err
}
return int(ver), nil
}
@@ -7,18 +7,24 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
func TestDashboardVersionService(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService}
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
t.Run("Get dashboard version", func(t *testing.T) {
dashboard := &dashver.DashboardVersion{
@@ -32,6 +38,44 @@ func TestDashboardVersionService(t *testing.T) {
require.NoError(t, err)
require.Equal(t, dashboard.ToDTO("uid"), dashboardVersion)
})
t.Run("Get dashboard version through k8s", func(t *testing.T) {
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
mockCli := new(client.MockK8sHandler)
dashboardVersionService.k8sclient = mockCli
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil)
mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&user.User{}, nil)
mockCli.On("Get", mock.Anything, "uid", int64(1), v1.GetOptions{ResourceVersion: "10"}, mock.Anything).Return(&unstructured.Unstructured{
Object: map[string]any{
"metadata": map[string]any{
"name": "uid",
"resourceVersion": "12",
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{
"version": int64(10),
},
}}, nil).Once()
res, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{
DashboardID: 42,
OrgID: 1,
Version: 10,
})
require.Nil(t, err)
require.Equal(t, res, &dashver.DashboardVersionDTO{
ID: 12, // RV should be used
Version: 10,
ParentVersion: 9,
DashboardID: 42,
DashboardUID: "uid",
Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(10)}),
})
})
}
func TestDeleteExpiredVersions(t *testing.T) {
@@ -42,7 +86,7 @@ func TestDeleteExpiredVersions(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{
cfg: cfg, store: dashboardVersionStore, dashSvc: dashboardService}
cfg: cfg, store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
t.Run("Don't delete anything if there are no expired versions", func(t *testing.T) {
err := dashboardVersionService.DeleteExpired(context.Background(), &dashver.DeleteExpiredVersionsCommand{DeletedRows: 4})
@@ -67,7 +111,7 @@ func TestListDashboardVersions(t *testing.T) {
t.Run("List all versions for a given Dashboard ID", func(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService}
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{
{ID: 1, DashboardID: 42},
}
@@ -78,15 +122,15 @@ func TestListDashboardVersions(t *testing.T) {
query := dashver.ListDashboardVersionsQuery{DashboardID: 42}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res))
require.Equal(t, 1, len(res.Versions))
// validate that the UID was populated
require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res)
require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res)
})
t.Run("List all versions for a non-existent DashboardID", func(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()}
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{
{ID: 1, DashboardID: 42},
}
@@ -96,15 +140,15 @@ func TestListDashboardVersions(t *testing.T) {
query := dashver.ListDashboardVersionsQuery{DashboardID: 42}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res))
require.Equal(t, 1, len(res.Versions))
// The DashboardID remains populated with the given value, even though the dash was not found
require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42}}, res)
require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42}}}, res)
})
t.Run("List all versions for a given DashboardUID", func(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()}
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}}
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).
Return(&dashboards.Dashboard{ID: 42}, nil)
@@ -112,15 +156,15 @@ func TestListDashboardVersions(t *testing.T) {
query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res))
require.Equal(t, 1, len(res.Versions))
// validate that the dashboardID was populated from the GetDashboard method call.
require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res)
require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res)
})
t.Run("List all versions for a given non-existent DashboardUID", func(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()}
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}}
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).
Return(nil, dashboards.ErrDashboardNotFound)
@@ -128,15 +172,15 @@ func TestListDashboardVersions(t *testing.T) {
query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res))
require.Equal(t, 1, len(res.Versions))
// validate that the dashboardUID & ID are populated, even though the dash was not found
require.EqualValues(t, []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}, res)
require.EqualValues(t, &dashver.DashboardVersionResponse{Versions: []*dashver.DashboardVersionDTO{{ID: 1, DashboardID: 42, DashboardUID: "uid"}}}, res)
})
t.Run("List Dashboard versions - error from store", func(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger()}
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, log: log.NewNopLogger(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedError = dashver.ErrDashboardVersionNotFound
query := dashver.ListDashboardVersionsQuery{DashboardID: 42, DashboardUID: "42"}
@@ -144,6 +188,46 @@ func TestListDashboardVersions(t *testing.T) {
require.Nil(t, res)
require.ErrorIs(t, err, dashver.ErrDashboardVersionNotFound)
})
t.Run("List all versions for a given Dashboard ID through k8s", func(t *testing.T) {
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
mockCli := new(client.MockK8sHandler)
dashboardVersionService.k8sclient = mockCli
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
dashboardService.On("GetDashboardUIDByID", mock.Anything,
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
query := dashver.ListDashboardVersionsQuery{DashboardID: 42}
mockCli.On("GetUserFromMeta", mock.Anything, mock.Anything).Return(&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{
"name": "uid",
"resourceVersion": "12",
"labels": map[string]any{
utils.LabelKeyDeprecatedInternalID: "42", // nolint:staticcheck
},
},
"spec": map[string]any{
"version": int64(5),
},
}}}}, nil).Once()
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res.Versions))
require.EqualValues(t, &dashver.DashboardVersionResponse{
Versions: []*dashver.DashboardVersionDTO{{
ID: 12, // should take rv
DashboardID: 42,
ParentVersion: 4,
Version: 5, // should take from spec
DashboardUID: "uid",
Data: simplejson.NewFromAny(map[string]any{"uid": "uid", "version": int64(5)}),
}}}, res)
})
}
type FakeDashboardVersionStore struct {
@@ -34,7 +34,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) {
query := dashver.GetDashboardVersionQuery{
DashboardID: savedDash.ID,
Version: savedDash.Version,
Version: int64(savedDash.Version),
OrgID: 1,
}
@@ -60,7 +60,7 @@ func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) {
t.Run("Attempt to get a version that doesn't exist", func(t *testing.T) {
query := dashver.GetDashboardVersionQuery{
DashboardID: int64(999),
Version: 123,
Version: int64(123),
OrgID: 1,
}
@@ -10,6 +10,7 @@ type FakeDashboardVersionService struct {
ExpectedDashboardVersion *dashver.DashboardVersionDTO
ExpectedDashboardVersions []*dashver.DashboardVersionDTO
ExpectedListDashboarVersions []*dashver.DashboardVersionDTO
ExpectedContinueToken string
counter int
ExpectedError error
}
@@ -30,6 +31,9 @@ func (f *FakeDashboardVersionService) DeleteExpired(ctx context.Context, cmd *da
return f.ExpectedError
}
func (f *FakeDashboardVersionService) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) ([]*dashver.DashboardVersionDTO, error) {
return f.ExpectedListDashboarVersions, f.ExpectedError
func (f *FakeDashboardVersionService) List(ctx context.Context, query *dashver.ListDashboardVersionsQuery) (*dashver.DashboardVersionResponse, error) {
return &dashver.DashboardVersionResponse{
ContinueToken: f.ExpectedContinueToken,
Versions: f.ExpectedListDashboarVersions,
}, f.ExpectedError
}
+18 -6
View File
@@ -52,7 +52,7 @@ type GetDashboardVersionQuery struct {
DashboardID int64
DashboardUID string
OrgID int64
Version int
Version int64
}
type DeleteExpiredVersionsCommand struct {
@@ -60,12 +60,19 @@ type DeleteExpiredVersionsCommand struct {
}
type ListDashboardVersionsQuery struct {
DashboardID int64
DashboardUID string
OrgID int64
Limit int
Start int
DashboardID int64
DashboardUID string
OrgID int64
Limit int
Start int
ContinueToken string
}
type DashboardVersionResponse struct {
ContinueToken string `json:"continueToken"`
Versions []*DashboardVersionDTO `json:"versions"`
}
type DashboardVersionDTO struct {
ID int64 `json:"id"`
DashboardID int64 `json:"dashboardId"`
@@ -94,3 +101,8 @@ type DashboardVersionMeta struct {
Data *simplejson.Json `json:"data"`
CreatedBy string `json:"createdBy"`
}
type DashboardVersionResponseMeta struct {
ContinueToken string `json:"continueToken"`
Versions []DashboardVersionMeta `json:"versions"`
}