k8s: Dashboards: allow querying of unistore (#97995)

pull/98129/head
Stephanie Hingtgen 6 months ago committed by GitHub
parent d8ddbcda51
commit b3985a4d37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      pkg/api/dashboard.go
  2. 6
      pkg/api/dashboard_test.go
  3. 2
      pkg/api/folder_bench_test.go
  4. 2
      pkg/services/dashboards/dashboard.go
  5. 10
      pkg/services/dashboards/dashboard_service_mock.go
  6. 434
      pkg/services/dashboards/service/dashboard_service.go
  7. 5
      pkg/services/dashboards/service/dashboard_service_integration_test.go
  8. 365
      pkg/services/dashboards/service/dashboard_service_test.go
  9. 1
      pkg/services/dashboards/service/zanzana_integration_test.go
  10. 2
      pkg/services/dashboardsnapshots/service/service_test.go
  11. 7
      pkg/services/folder/folderimpl/folder_test.go
  12. 4
      pkg/services/libraryelements/libraryelements_test.go
  13. 4
      pkg/services/librarypanels/librarypanels_test.go
  14. 2
      pkg/services/ngalert/testutil/testutil.go
  15. 4
      pkg/services/plugindashboards/service/dashboard_updater.go
  16. 22
      pkg/services/plugindashboards/service/dashboard_updater_test.go
  17. 2
      pkg/services/publicdashboards/api/query_test.go

@ -478,7 +478,7 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo
hs.log.Error("Failed to delete public dashboard")
}
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, c.SignedInUser.GetOrgID())
err = hs.DashboardService.DeleteDashboard(c.Req.Context(), dash.ID, dash.UID, c.SignedInUser.GetOrgID())
if err != nil {
var dashboardErr dashboards.DashboardErr
if ok := errors.As(err, &dashboardErr); ok {

@ -263,7 +263,7 @@ func TestHTTPServer_DeleteDashboardByUID_AccessControl(t *testing.T) {
dashSvc := dashboards.NewFakeDashboardService(t)
dashSvc.On("GetDashboard", mock.Anything, mock.Anything).Return(dash, nil).Maybe()
dashSvc.On("DeleteDashboard", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
dashSvc.On("DeleteDashboard", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
hs.DashboardService = dashSvc
hs.Cfg = setting.NewCfg()
@ -838,14 +838,14 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
if dashboardService == nil {
dashboardService, err = service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil,
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil,
)
require.NoError(t, err)
}
dashboardProvisioningService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore, features, folderPermissions, dashboardPermissions,
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil,
ac, folderSvc, fStore, nil, zanzana.NewNoopClient(), nil, nil,
)
require.NoError(t, err)

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

@ -15,7 +15,7 @@ import (
//go:generate mockery --name DashboardService --structname FakeDashboardService --inpackage --filename dashboard_service_mock.go
type DashboardService interface {
BuildSaveDashboardCommand(ctx context.Context, dto *SaveDashboardDTO, validateProvisionedDashboard bool) (*SaveDashboardCommand, error)
DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error
DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error
FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)
// GetDashboard fetches a dashboard.
// To fetch a dashboard under root by title should set the folder UID to point to an empty string

@ -102,17 +102,17 @@ func (_m *FakeDashboardService) CountInFolders(ctx context.Context, orgID int64,
return r0, r1
}
// DeleteDashboard provides a mock function with given fields: ctx, dashboardId, orgId
func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
ret := _m.Called(ctx, dashboardId, orgId)
// DeleteDashboard provides a mock function with given fields: ctx, dashboardId, dashboardUID, orgId
func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
ret := _m.Called(ctx, dashboardId, dashboardUID, orgId)
if len(ret) == 0 {
panic("no return value specified for DeleteDashboard")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
r0 = rf(ctx, dashboardId, orgId)
if rf, ok := ret.Get(0).(func(context.Context, int64, string, int64) error); ok {
r0 = rf(ctx, dashboardId, dashboardUID, orgId)
} else {
r0 = ret.Error(0)
}

@ -2,11 +2,13 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/grafana/authlib/claims"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
@ -38,6 +40,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
k8sUser "k8s.io/apiserver/pkg/authentication/user"
@ -67,6 +70,7 @@ type DashboardServiceImpl struct {
dashboardStore dashboards.Store
folderStore folder.FolderStore
folderService folder.Service
userService user.Service
features featuremgmt.FeatureToggles
folderPermissions accesscontrol.FolderPermissionsService
dashboardPermissions accesscontrol.DashboardPermissionsService
@ -79,6 +83,7 @@ type DashboardServiceImpl struct {
// interface to allow for testing
type dashboardK8sHandler interface {
getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool)
getNamespace(orgID int64) string
}
var _ dashboardK8sHandler = (*dashk8sHandler)(nil)
@ -95,15 +100,10 @@ func ProvideDashboardServiceImpl(
features featuremgmt.FeatureToggles, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, ac accesscontrol.AccessControl,
folderSvc folder.Service, fStore folder.Store, r prometheus.Registerer, zclient zanzana.Client,
restConfigProvider apiserver.RestConfigProvider,
restConfigProvider apiserver.RestConfigProvider, userService user.Service,
) (*DashboardServiceImpl, error) {
gvr := schema.GroupVersionResource{
Group: v0alpha1.GROUP,
Version: v0alpha1.VERSION,
Resource: v0alpha1.DashboardResourceInfo.GetName(),
}
k8sHandler := &dashk8sHandler{
gvr: gvr,
gvr: v0alpha1.DashboardResourceInfo.GroupVersionResource(),
namespacer: request.GetNamespaceMapper(cfg),
restConfigProvider: restConfigProvider,
}
@ -119,6 +119,7 @@ func ProvideDashboardServiceImpl(
zclient: zclient,
folderStore: folderStore,
folderService: folderSvc,
userService: userService,
k8sclient: k8sHandler,
metrics: newDashboardsMetrics(r),
}
@ -142,6 +143,7 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx con
}
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
// TODO: make this go through the k8s cli too under the feature toggle. First get dashboard through unistore & then get provisioning data
return dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID)
}
@ -284,6 +286,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
}
func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
// TODO: once we can search in unistore by id, go through k8s cli too
return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd)
}
@ -358,6 +361,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
}
// dashboard
// TODO: make this go through the k8s cli too under the feature toggle. First save dashboard & then save provisioning data
dash, err := dr.dashboardStore.SaveProvisionedDashboard(ctx, *cmd, provisioning)
if err != nil {
return nil, err
@ -402,9 +406,9 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
return nil, err
}
dash, err := dr.dashboardStore.SaveDashboard(ctx, *cmd)
dash, err := dr.saveDashboard(ctx, cmd)
if err != nil {
return nil, fmt.Errorf("saving dashboard failed: %w", err)
return nil, err
}
// new dashboard created
@ -414,7 +418,20 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
return dash, nil
}
func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID)
}
return dr.dashboardStore.SaveDashboard(ctx, *cmd)
}
func (dr *DashboardServiceImpl) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
return dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: orgID, UID: uid, IncludeDeleted: true})
}
return dr.dashboardStore.GetSoftDeletedDashboard(ctx, orgID, uid)
}
@ -457,6 +474,8 @@ func (dr *DashboardServiceImpl) RestoreDashboard(ctx context.Context, dashboard
return folder.ErrInternal.Errorf("failed to fetch parent folder from store: %w", err)
}
// TODO: once restore in k8s is finalized, add functionality here under the feature toggle
return dr.dashboardStore.RestoreDashboard(ctx, dashboard.OrgID, dashboard.UID, restoringFolder)
}
@ -473,13 +492,18 @@ func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID i
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
}
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
// deletes in unistore are soft deletes, so we can just delete in the same way
return dr.deleteDashboardThroughK8s(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgID, UID: dashboardUID})
}
return dr.dashboardStore.SoftDeleteDashboard(ctx, orgID, dashboardUID)
}
// DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for
// operations by the user where we want to make sure user does not delete provisioned dashboard.
func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
return dr.deleteDashboard(ctx, dashboardId, orgId, true)
func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
return dr.deleteDashboard(ctx, dashboardId, dashboardUID, orgId, true)
}
func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*dashboards.Dashboard, error) {
@ -488,10 +512,10 @@ func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, das
// DeleteProvisionedDashboard removes dashboard from the DB even if it is provisioned.
func (dr *DashboardServiceImpl) DeleteProvisionedDashboard(ctx context.Context, dashboardId int64, orgId int64) error {
return dr.deleteDashboard(ctx, dashboardId, orgId, false)
return dr.deleteDashboard(ctx, dashboardId, "", orgId, false)
}
func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, orgId int64, validateProvisionedDashboard bool) error {
func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId int64, dashboardUID string, orgId int64, validateProvisionedDashboard bool) error {
ctx, span := tracer.Start(ctx, "dashboards.service.deleteDashboard")
defer span.End()
@ -505,7 +529,14 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
}
}
cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId}
cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID}
// TODO: once we can do this search by IDs in unistore, remove this constraint
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) && cmd.UID != "" {
return dr.deleteDashboardThroughK8s(ctx, cmd)
}
return dr.dashboardStore.DeleteDashboard(ctx, cmd)
}
@ -526,7 +557,7 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo
return nil, err
}
dash, err := dr.dashboardStore.SaveDashboard(ctx, *cmd)
dash, err := dr.saveDashboard(ctx, cmd)
if err != nil {
return nil, err
}
@ -539,10 +570,12 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo
// UnprovisionDashboard removes info about dashboard being provisioned. Used after provisioning configs are changed
// and provisioned dashboards are left behind but not deleted.
func (dr *DashboardServiceImpl) UnprovisionDashboard(ctx context.Context, dashboardId int64) error {
// TODO: once we can search by dashboard ID in unistore, go through k8s cli too
return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId)
}
func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) {
// TODO: once we can do this search in unistore, go through k8s cli too
return dr.dashboardStore.GetDashboardsByPluginID(ctx, query)
}
@ -626,92 +659,20 @@ func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context,
}
}
func (dk8s *dashk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
dyn, err := dynamic.NewForConfig(dk8s.restConfigProvider.GetRestConfig(ctx))
if err != nil {
return nil, false
}
return dyn.Resource(dk8s.gvr).Namespace(dk8s.namespacer(orgID)), true
}
func (dr *DashboardServiceImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) {
requester, requesterErr := identity.GetRequester(ctx)
if requesterErr != nil {
return nil, nil, requesterErr
}
user, exists := k8sRequest.UserFrom(ctx)
if !exists {
// add in k8s user if not there yet
var ok bool
user, ok = requester.(k8sUser.Info)
if !ok {
return nil, nil, fmt.Errorf("could not convert user to k8s user")
}
}
newCtx := k8sRequest.WithUser(context.Background(), user)
newCtx = log.WithContextualAttributes(newCtx, log.FromContext(ctx))
// TODO: after GLSA token workflow is removed, make this return early
// and move the else below to be unconditional
if requesterErr == nil {
newCtxWithRequester := identity.WithRequester(newCtx, requester)
newCtx = newCtxWithRequester
}
// inherit the deadline from the original context, if it exists
deadline, ok := ctx.Deadline()
if ok {
var newCancel context.CancelFunc
newCtx, newCancel = context.WithTimeout(newCtx, time.Until(deadline))
return newCtx, newCancel, nil
}
return newCtx, nil, nil
}
func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
// TODO: once getting dashboards by ID in unified storage is supported, we can remove the restraint of the uid being provided
// TODO: once we can do this search by ID in unistore, remove this constraint
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) && query.UID != "" {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := dr.k8sclient.getClient(newCtx, query.OrgID)
if !ok {
return nil, nil
}
// if including deleted dashboards, use the /latest subresource
subresource := ""
if query.IncludeDeleted {
subresource = "latest"
}
out, err := client.Get(newCtx, query.UID, v1.GetOptions{}, subresource)
if err != nil {
return nil, err
} else if out == nil {
return nil, dashboards.ErrDashboardNotFound
}
return UnstructuredToLegacyDashboard(out, query.OrgID)
return dr.getDashboardThroughK8s(ctx, query)
}
return dr.dashboardStore.GetDashboard(ctx, query)
}
// TODO: once getting dashboards by ID in unified storage is supported, need to do the same as the above function
// 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) {
return dr.dashboardStore.GetDashboardUIDByID(ctx, query)
}
// TODO: add support to get dashboards in unified storage.
func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
return dr.dashboardStore.GetDashboards(ctx, query)
}
@ -808,7 +769,7 @@ func (dr *DashboardServiceImpl) getUserSharedDashboardUIDs(ctx context.Context,
return userDashboardUIDs, nil
}
// TODO: add support to find dashboards in unified storage too.
// TODO: once we can do this search by this in unistore, go through k8s cli too
func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.FindDashboards")
defer span.End()
@ -833,6 +794,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
return dr.dashboardStore.FindDashboards(ctx, query)
}
// TODO: once we can do this search in unistore, go through k8s cli too
func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) {
ctx, span := tracer.Start(ctx, "dashboards.service.SearchDashboards")
defer span.End()
@ -854,6 +816,14 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das
}
func (dr *DashboardServiceImpl) GetAllDashboards(ctx context.Context) ([]*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
return dr.listDashboardThroughK8s(ctx, requester.GetOrgID())
}
return dr.dashboardStore.GetAllDashboards(ctx)
}
@ -915,6 +885,7 @@ 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
return dr.dashboardStore.GetDashboardTags(ctx, query)
}
@ -945,7 +916,7 @@ func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (i
return 0, err
}
for _, dashboard := range deletedDashboards {
err = dr.DeleteDashboard(ctx, dashboard.ID, dashboard.OrgID)
err = dr.DeleteDashboard(ctx, dashboard.ID, dashboard.UID, dashboard.OrgID)
if err != nil {
dr.log.Warn("Failed to cleanup deleted dashboard", "dashboardUid", dashboard.UID, "error", err)
break
@ -956,35 +927,247 @@ func (dr *DashboardServiceImpl) CleanUpDeletedDashboards(ctx context.Context) (i
return deletedDashboardsCount, nil
}
func UnstructuredToLegacyDashboard(item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) {
spec, ok := item.Object["spec"].(map[string]any)
// -----------------------------------------------------------------------------------------
// Dashboard k8s functions
// -----------------------------------------------------------------------------------------
func (dk8s *dashk8sHandler) getClient(ctx context.Context, orgID int64) (dynamic.ResourceInterface, bool) {
dyn, err := dynamic.NewForConfig(dk8s.restConfigProvider.GetRestConfig(ctx))
if err != nil {
return nil, false
}
return dyn.Resource(dk8s.gvr).Namespace(dk8s.getNamespace(orgID)), true
}
func (dk8s *dashk8sHandler) getNamespace(orgID int64) string {
return dk8s.namespacer(orgID)
}
func (dr *DashboardServiceImpl) getK8sContext(ctx context.Context) (context.Context, context.CancelFunc, error) {
requester, requesterErr := identity.GetRequester(ctx)
if requesterErr != nil {
return nil, nil, requesterErr
}
user, exists := k8sRequest.UserFrom(ctx)
if !exists {
// add in k8s user if not there yet
var ok bool
user, ok = requester.(k8sUser.Info)
if !ok {
return nil, nil, fmt.Errorf("could not convert user to k8s user")
}
}
newCtx := k8sRequest.WithUser(context.Background(), user)
newCtx = log.WithContextualAttributes(newCtx, log.FromContext(ctx))
// TODO: after GLSA token workflow is removed, make this return early
// and move the else below to be unconditional
if requesterErr == nil {
newCtxWithRequester := identity.WithRequester(newCtx, requester)
newCtx = newCtxWithRequester
}
// inherit the deadline from the original context, if it exists
deadline, ok := ctx.Deadline()
if ok {
var newCancel context.CancelFunc
newCtx, newCancel = context.WithTimeout(newCtx, time.Until(deadline))
return newCtx, newCancel, nil
}
return newCtx, nil, nil
}
func (dr *DashboardServiceImpl) getDashboardThroughK8s(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := dr.k8sclient.getClient(newCtx, query.OrgID)
if !ok {
return nil, errors.New("error parsing dashboard from k8s response")
return nil, nil
}
out := dashboards.Dashboard{
OrgID: orgID,
Data: simplejson.NewFromAny(spec),
// if including deleted dashboards, use the /latest subresource
subresource := ""
if query.IncludeDeleted {
subresource = "latest"
}
out, err := client.Get(newCtx, query.UID, v1.GetOptions{}, subresource)
if err != nil {
return nil, err
} else if out == nil {
return nil, dashboards.ErrDashboardNotFound
}
return dr.UnstructuredToLegacyDashboard(ctx, out, query.OrgID)
}
func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd *dashboards.SaveDashboardCommand, orgID int64) (*dashboards.Dashboard, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := dr.k8sclient.getClient(newCtx, orgID)
if !ok {
return nil, nil
}
obj, err := LegacySaveCommandToUnstructured(cmd, dr.k8sclient.getNamespace(orgID))
if err != nil {
return nil, err
}
var out *unstructured.Unstructured
current, err := client.Get(newCtx, obj.GetName(), v1.GetOptions{})
if current == nil || err != nil {
out, err = client.Create(newCtx, &obj, v1.CreateOptions{})
if err != nil {
return nil, err
}
} else {
out, err = client.Update(newCtx, &obj, v1.UpdateOptions{})
if err != nil {
return nil, err
}
}
finalDash, err := dr.UnstructuredToLegacyDashboard(ctx, out, orgID)
if err != nil {
return nil, err
}
return finalDash, nil
}
func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx)
if err != nil {
return err
} else if cancel != nil {
defer cancel()
}
client, ok := dr.k8sclient.getClient(newCtx, cmd.OrgID)
if !ok {
return fmt.Errorf("could not get k8s client")
}
err = client.Delete(newCtx, cmd.UID, v1.DeleteOptions{})
if err != nil {
return err
}
return nil
}
func (dr *DashboardServiceImpl) listDashboardThroughK8s(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx)
if err != nil {
return nil, err
} else if cancel != nil {
defer cancel()
}
client, ok := dr.k8sclient.getClient(newCtx, orgID)
if !ok {
return nil, nil
}
// TODO: once we can do this search in unistore, update this
out, err := client.List(newCtx, v1.ListOptions{})
if err != nil {
return nil, err
} else if out == nil {
return nil, dashboards.ErrDashboardNotFound
}
dashboards := make([]*dashboards.Dashboard, 0)
for _, item := range out.Items {
dash, err := dr.UnstructuredToLegacyDashboard(ctx, &item, orgID)
if err != nil {
return nil, err
}
dashboards = append(dashboards, dash)
}
return dashboards, nil
}
func (dr *DashboardServiceImpl) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*dashboards.Dashboard, error) {
spec, ok := item.Object["spec"].(map[string]any)
if !ok {
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
if version, ok := spec["version"].(int64); ok {
dashVersion = int(version)
}
out.UID = obj.GetName()
out.Slug = obj.GetSlug()
out.FolderUID = obj.GetFolder()
out := dashboards.Dashboard{
OrgID: orgID,
UID: uid,
Slug: obj.GetSlug(),
FolderUID: obj.GetFolder(),
Version: dashVersion,
Data: simplejson.NewFromAny(spec),
}
out.Created = obj.GetCreationTimestamp().Time
updated, err := obj.GetUpdatedTimestamp()
if err == nil && updated != nil {
out.Updated = *updated
} else {
// by default, set updated to created
out.Updated = out.Created
}
deleted := obj.GetDeletionTimestamp()
if deleted != nil {
out.Deleted = obj.GetDeletionTimestamp().Time
}
createdBy := obj.GetCreatedBy()
if createdBy != "" && toUID(createdBy) != "" {
creator, err := dr.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: toUID(createdBy)})
if err != nil {
return nil, err
}
out.CreatedBy = creator.ID
}
updatedBy := obj.GetUpdatedBy()
if updatedBy != "" && toUID(updatedBy) != "" {
updator, err := dr.userService.GetByUID(ctx, &user.GetUserByUIDQuery{UID: toUID(updatedBy)})
if err != nil {
return nil, err
}
out.UpdatedBy = updator.ID
}
if id, ok := spec["id"].(int64); ok {
out.ID = id
}
@ -993,10 +1176,6 @@ func UnstructuredToLegacyDashboard(item *unstructured.Unstructured, orgID int64)
out.GnetID = gnetID
}
if version, ok := spec["version"].(int64); ok {
out.Version = int(version)
}
if pluginID, ok := spec["plugin_id"].(string); ok {
out.PluginID = pluginID
}
@ -1019,3 +1198,48 @@ func UnstructuredToLegacyDashboard(item *unstructured.Unstructured, orgID int64)
return &out, nil
}
func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, namespace string) (unstructured.Unstructured, error) {
uid := cmd.GetDashboardModel().UID
if uid == "" {
uid = uuid.NewString()
}
finalObj := unstructured.Unstructured{
Object: map[string]interface{}{},
}
obj := map[string]interface{}{}
body, err := cmd.Dashboard.ToDB()
if err != nil {
return finalObj, err
}
err = json.Unmarshal(body, &obj)
if err != nil {
return finalObj, err
}
// update the version
version, ok := obj["version"].(float64)
if !ok || version == 0 {
obj["version"] = 1
} else if !cmd.Overwrite {
obj["version"] = version + 1
}
finalObj.Object["spec"] = obj
finalObj.SetName(uid)
finalObj.SetNamespace(namespace)
finalObj.SetGroupVersionKind(v0alpha1.DashboardResourceInfo.GroupVersionKind())
return finalObj, nil
}
func toUID(rawIdentifier string) string {
parts := strings.Split(rawIdentifier, ":")
if len(parts) < 2 {
return ""
}
return parts[1]
}

@ -886,6 +886,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)
guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
@ -954,6 +955,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false)
@ -981,6 +983,7 @@ func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSto
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)
_, err = service.SaveDashboard(context.Background(), &dto, false)
@ -1027,6 +1030,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false)
@ -1080,6 +1084,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)
res, err := service.SaveDashboard(context.Background(), &dto, false)

@ -4,6 +4,7 @@ import (
"context"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -11,6 +12,7 @@ import (
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"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/dashboards"
@ -19,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -179,7 +182,7 @@ func TestDashboardService(t *testing.T) {
t.Run("DeleteDashboard should fail to delete it when provisioning information is missing", func(t *testing.T) {
fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(&dashboards.DashboardProvisioning{}, nil).Once()
err := service.DeleteDashboard(context.Background(), 1, 1)
err := service.DeleteDashboard(context.Background(), 1, "", 1)
require.Equal(t, err, dashboards.ErrDashboardCannotDeleteProvisionedDashboard)
})
})
@ -196,7 +199,7 @@ func TestDashboardService(t *testing.T) {
args := &dashboards.DeleteDashboardCommand{OrgID: 1, ID: 1}
fakeStore.On("DeleteDashboard", mock.Anything, args).Return(nil).Once()
fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.AnythingOfType("int64")).Return(nil, nil).Once()
err := service.DeleteDashboard(context.Background(), 1, 1)
err := service.DeleteDashboard(context.Background(), 1, "", 1)
require.NoError(t, err)
})
})
@ -238,6 +241,10 @@ func (m *mockDashK8sCli) getClient(ctx context.Context, orgID int64) (dynamic.Re
return args.Get(0).(dynamic.ResourceInterface), args.Bool(1)
}
func (m *mockDashK8sCli) getNamespace(orgID int64) string {
return "default"
}
type mockResourceInterface struct {
mock.Mock
dynamic.ResourceInterface
@ -251,6 +258,48 @@ func (m *mockResourceInterface) Get(ctx context.Context, name string, options me
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}
func (m *mockResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
args := m.Called(ctx, opts)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*unstructured.UnstructuredList), args.Error(1)
}
func (m *mockResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
args := m.Called(ctx, obj, options, subresources)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}
func (m *mockResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
args := m.Called(ctx, obj, options, subresources)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
}
func (m *mockResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
args := m.Called(ctx, name, options, subresources)
return args.Error(0)
}
func setupK8sDashboardTests(service *DashboardServiceImpl) (context.Context, *mockDashK8sCli, *mockResourceInterface) {
k8sClientMock := new(mockDashK8sCli)
k8sResourceMock := new(mockResourceInterface)
service.k8sclient = k8sClientMock
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
ctx := context.Background()
userCtx := &user.SignedInUser{UserID: 1, OrgID: 1}
ctx = identity.WithRequester(ctx, userCtx)
return ctx, k8sClientMock, k8sResourceMock
}
func TestGetDashboard(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
@ -258,14 +307,13 @@ func TestGetDashboard(t *testing.T) {
cfg: setting.NewCfg(),
dashboardStore: &fakeStore,
}
query := &dashboards.GetDashboardQuery{
UID: "test-uid",
OrgID: 1,
}
t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) {
service.features = featuremgmt.WithFeatures()
query := &dashboards.GetDashboardQuery{
UID: "test-uid",
OrgID: 1,
}
fakeStore.On("GetDashboard", mock.Anything, query).Return(&dashboards.Dashboard{}, nil).Once()
dashboard, err := service.GetDashboard(context.Background(), query)
require.NoError(t, err)
@ -274,36 +322,26 @@ func TestGetDashboard(t *testing.T) {
})
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
k8sClientMock := new(mockDashK8sCli)
k8sResourceMock := new(mockResourceInterface)
service.k8sclient = k8sClientMock
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
query := &dashboards.GetDashboardQuery{
UID: "test-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",
"title": "testing slugify",
"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
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify"}),
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)}),
}
ctx := context.Background()
userCtx := &user.SignedInUser{UserID: 1}
ctx = identity.WithRequester(ctx, userCtx)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once()
k8sResourceMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil).Once()
@ -316,18 +354,7 @@ func TestGetDashboard(t *testing.T) {
})
t.Run("Should return error when Kubernetes client fails", func(t *testing.T) {
k8sClientMock := new(mockDashK8sCli)
k8sResourceMock := new(mockResourceInterface)
service.k8sclient = k8sClientMock
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
query := &dashboards.GetDashboardQuery{
UID: "test-uid",
OrgID: 1,
}
ctx := context.Background()
userCtx := &user.SignedInUser{UserID: 1}
ctx = identity.WithRequester(ctx, userCtx)
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once()
k8sResourceMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
@ -338,21 +365,9 @@ func TestGetDashboard(t *testing.T) {
})
t.Run("Should return dashboard not found if Kubernetes client returns nil", func(t *testing.T) {
k8sClientMock := new(mockDashK8sCli)
k8sResourceMock := new(mockResourceInterface)
service.k8sclient = k8sClientMock
service.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesCliDashboards)
query := &dashboards.GetDashboardQuery{
UID: "test-uid",
OrgID: 1,
}
ctx := context.Background()
userCtx := &user.SignedInUser{UserID: 1}
ctx = identity.WithRequester(ctx, userCtx)
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once()
k8sResourceMock.On("Get", mock.Anything, query.UID, mock.Anything, mock.Anything).Return(nil, nil).Once()
dashboard, err := service.GetDashboard(ctx, query)
require.Error(t, err)
require.Equal(t, dashboards.ErrDashboardNotFound, err)
@ -360,3 +375,249 @@ func TestGetDashboard(t *testing.T) {
k8sClientMock.AssertExpectations(t)
})
}
func TestGetAllDashboards(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
service := &DashboardServiceImpl{
cfg: setting.NewCfg(),
dashboardStore: &fakeStore,
}
t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) {
service.features = featuremgmt.WithFeatures()
fakeStore.On("GetAllDashboards", mock.Anything).Return([]*dashboards.Dashboard{}, nil).Once()
dashboard, err := service.GetAllDashboards(context.Background())
require.NoError(t, err)
require.NotNil(t, dashboard)
fakeStore.AssertExpectations(t)
})
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
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, // default to 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("List", mock.Anything, mock.Anything).Return(&unstructured.UnstructuredList{Items: []unstructured.Unstructured{dashboardUnstructured}}, nil).Once()
dashes, err := service.GetAllDashboards(ctx)
require.NoError(t, err)
require.NotNil(t, dashes)
k8sClientMock.AssertExpectations(t)
// make sure the conversion is working
require.True(t, reflect.DeepEqual(dashes, []*dashboards.Dashboard{&dashboardExpected}))
})
}
func TestSaveDashboard(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
service := &DashboardServiceImpl{
cfg: setting.NewCfg(),
dashboardStore: &fakeStore,
}
origNewDashboardGuardian := guardian.New
defer func() { guardian.New = origNewDashboardGuardian }()
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
query := &dashboards.SaveDashboardDTO{
OrgID: 1,
User: &user.SignedInUser{UserID: 1},
Dashboard: &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
Data: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "uid"}),
},
}
t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) {
service.features = featuremgmt.WithFeatures()
fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil)
fakeStore.On("ValidateDashboardBeforeSave", mock.Anything, mock.Anything, mock.AnythingOfType("bool")).Return(true, nil)
fakeStore.On("SaveDashboard", mock.Anything, mock.Anything, mock.Anything).Return(&dashboards.Dashboard{}, nil)
dashboard, err := service.SaveDashboard(context.Background(), query, false)
require.NoError(t, err)
require.NotNil(t, dashboard)
fakeStore.AssertExpectations(t)
})
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",
},
}}
t.Run("Should use Kubernetes create if feature flags are enabled and dashboard doesn't exist", func(t *testing.T) {
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true)
k8sResourceMock.On("Get", mock.Anything, query.Dashboard.UID, mock.Anything, mock.Anything).Return(nil, nil)
k8sResourceMock.On("Create", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
dashboard, err := service.SaveDashboard(ctx, query, false)
require.NoError(t, err)
require.NotNil(t, dashboard)
})
t.Run("Should use Kubernetes update if feature flags are enabled and dashboard exists", func(t *testing.T) {
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true)
k8sResourceMock.On("Get", mock.Anything, query.Dashboard.UID, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
k8sResourceMock.On("Update", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&dashboardUnstructured, nil)
dashboard, err := service.SaveDashboard(ctx, query, false)
require.NoError(t, err)
require.NotNil(t, dashboard)
})
}
func TestDeleteDashboard(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t)
service := &DashboardServiceImpl{
cfg: setting.NewCfg(),
dashboardStore: &fakeStore,
}
t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) {
service.features = featuremgmt.WithFeatures()
fakeStore.On("DeleteDashboard", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil).Once()
err := service.DeleteDashboard(context.Background(), 1, "uid", 1)
require.NoError(t, err)
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()
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()
err := service.DeleteDashboard(ctx, 1, "uid", 1)
require.NoError(t, err)
k8sClientMock.AssertExpectations(t)
})
}
func TestUnstructuredToLegacyDashboard(t *testing.T) {
fake := usertest.NewUserServiceFake()
fake.ExpectedUser = &user.User{ID: 10, UID: "useruid"}
dr := &DashboardServiceImpl{
userService: fake,
}
t.Run("successfully converts unstructured to legacy dashboard", func(t *testing.T) {
uid := "36b7c825-79cc-435e-acf6-c78bd96a4510"
orgID := int64(123)
title := "Test Dashboard"
now := metav1.Now()
item := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
"title": title,
"version": int64(1),
"id": int64(1),
},
},
}
obj, err := utils.MetaAccessor(item)
require.NoError(t, err)
obj.SetCreationTimestamp(now)
obj.SetName(uid)
obj.SetCreatedBy("user:useruid")
obj.SetUpdatedBy("user:useruid")
result, err := dr.UnstructuredToLegacyDashboard(context.Background(), item, orgID)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, uid, result.UID)
assert.Equal(t, title, result.Title)
assert.Equal(t, orgID, result.OrgID)
assert.Equal(t, "test-dashboard", result.Slug) // should slugify the title
assert.Equal(t, false, result.HasACL)
assert.Equal(t, false, result.IsFolder)
assert.Equal(t, int64(1), result.ID)
assert.Equal(t, now.Time.Format(time.RFC3339), result.Created.Format(time.RFC3339))
assert.Equal(t, int64(10), result.CreatedBy)
assert.Equal(t, now.Time.Format(time.RFC3339), result.Updated.Format(time.RFC3339)) // updated should default to created
assert.Equal(t, int64(10), result.UpdatedBy)
})
t.Run("returns error if spec is missing", func(t *testing.T) {
item := &unstructured.Unstructured{
Object: map[string]interface{}{},
}
_, err := (&DashboardServiceImpl{}).UnstructuredToLegacyDashboard(context.Background(), item, int64(123))
assert.Error(t, err)
assert.Equal(t, "error parsing dashboard from k8s response", err.Error())
})
}
func TestLegacySaveCommandToUnstructured(t *testing.T) {
namespace := "test-namespace"
t.Run("successfully converts save command to unstructured", func(t *testing.T) {
cmd := &dashboards.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "test-uid"}),
}
result, err := LegacySaveCommandToUnstructured(cmd, namespace)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "test-uid", result.GetName())
assert.Equal(t, "test-namespace", result.GetNamespace())
spec := result.Object["spec"].(map[string]any)
assert.Equal(t, spec["version"], 1)
})
t.Run("should increase version when called", func(t *testing.T) {
cmd := &dashboards.SaveDashboardCommand{
Dashboard: simplejson.NewFromAny(map[string]any{"test": "test", "title": "testing slugify", "uid": "test-uid", "version": int64(1)}),
}
result, err := LegacySaveCommandToUnstructured(cmd, namespace)
assert.NoError(t, err)
assert.NotNil(t, result)
spec := result.Object["spec"].(map[string]any)
assert.Equal(t, spec["version"], float64(2))
})
}
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)
})
}

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

@ -101,7 +101,7 @@ func TestValidateDashboardExists(t *testing.T) {
feats := featuremgmt.WithFeatures()
dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, feats, tagimpl.ProvideService(sqlStore), quotatest.New(false, nil))
require.NoError(t, err)
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), feats, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil)
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashboardStore, folderimpl.ProvideDashboardFolderStore(sqlStore), feats, nil, nil, acmock.New(), foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil, nil)
require.NoError(t, err)
s := ProvideService(dsStore, secretsService, dashSvc)
ctx := context.Background()

@ -490,7 +490,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
CanEditValue: true,
})
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, zanzana.NewNoopClient(), nil)
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOn, folderPermissions, dashboardPermissions, ac, serviceWithFlagOn, nestedFolderStore, nil, zanzana.NewNoopClient(), nil, nil)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOn, db, serviceWithFlagOn, dashSrv, ac, b)
@ -573,7 +573,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
})
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuresFlagOff,
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, zanzana.NewNoopClient(), nil)
folderPermissions, dashboardPermissions, ac, serviceWithFlagOff, nestedFolderStore, nil, zanzana.NewNoopClient(), nil, nil)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, featuresFlagOff, db, serviceWithFlagOff, dashSrv, ac, b)
@ -719,7 +719,7 @@ func TestIntegrationNestedFolderService(t *testing.T) {
tc.service.dashboardStore = dashStore
tc.service.store = nestedFolderStore
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil, zanzana.NewNoopClient(), nil)
dashSrv, err := dashboardservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, tc.featuresFlag, folderPermissions, dashboardPermissions, ac, tc.service, tc.service.store, nil, zanzana.NewNoopClient(), nil, nil)
require.NoError(t, err)
alertStore, err := ngstore.ProvideDBStore(cfg, tc.featuresFlag, db, tc.service, dashSrv, ac, b)
require.NoError(t, err)
@ -1506,6 +1506,7 @@ func TestIntegrationNestedFolderSharedWithMe(t *testing.T) {
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)

@ -312,6 +312,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
nil,
zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, err)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
@ -400,6 +401,7 @@ func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scena
foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(),
nil,
nil,
)
require.NoError(t, svcErr)
guardian.InitAccessControlGuardian(cfg, ac, dashboardService)
@ -461,7 +463,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg, dashboardStore, folderStore,
features, folderPermissions, dashboardPermissions, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil,
nil, zanzana.NewNoopClient(), nil, nil,
)
require.NoError(t, dashSvcErr)
guardian.InitAccessControlGuardian(cfg, ac, dashService)

@ -736,7 +736,7 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
cfg, dashboardStore, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil,
nil, zanzana.NewNoopClient(), nil, nil,
)
require.NoError(t, err)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
@ -833,7 +833,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
cfg, dashStore, folderStore,
features, acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(),
nil, zanzana.NewNoopClient(), nil,
nil, zanzana.NewNoopClient(), nil, nil,
)
require.NoError(t, err)
guardian.InitAccessControlGuardian(cfg, ac, dashService)

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

@ -95,7 +95,7 @@ func (du *DashboardUpdater) syncPluginDashboards(ctx context.Context, plugin plu
if dash.Removed {
du.logger.Info("Deleting plugin dashboard", "pluginId", plugin.ID, "dashboard", dash.Slug)
if err := du.dashboardService.DeleteDashboard(ctx, dash.DashboardId, orgID); err != nil {
if err := du.dashboardService.DeleteDashboard(ctx, dash.DashboardId, dash.UID, orgID); err != nil {
du.logger.Error("Failed to auto update app dashboard", "pluginId", plugin.ID, "error", err)
return
}
@ -150,7 +150,7 @@ func (du *DashboardUpdater) handlePluginStateChanged(ctx context.Context, event
for _, dash := range queryResult {
du.logger.Info("Deleting plugin dashboard", "pluginId", event.PluginId, "dashboard", dash.Slug)
if err := du.dashboardService.DeleteDashboard(ctx, dash.ID, dash.OrgID); err != nil {
if err := du.dashboardService.DeleteDashboard(ctx, dash.ID, dash.UID, dash.OrgID); err != nil {
return err
}
}

@ -443,18 +443,21 @@ func (s *pluginsSettingsServiceMock) DecryptedValues(_ *pluginsettings.DTO) map[
type dashboardServiceMock struct {
dashboards.DashboardService
deleteDashboardArgs []struct {
orgId int64
dashboardId int64
orgId int64
dashboardId int64
dashboardUID string
}
}
func (s *dashboardServiceMock) DeleteDashboard(_ context.Context, dashboardId int64, orgId int64) error {
func (s *dashboardServiceMock) DeleteDashboard(_ context.Context, dashboardId int64, dashboardUID string, orgId int64) error {
s.deleteDashboardArgs = append(s.deleteDashboardArgs, struct {
orgId int64
dashboardId int64
orgId int64
dashboardId int64
dashboardUID string
}{
orgId: orgId,
dashboardId: dashboardId,
orgId: orgId,
dashboardId: dashboardId,
dashboardUID: dashboardUID,
})
return nil
}
@ -532,8 +535,9 @@ func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenar
sCtx.dashboardService = &dashboardServiceMock{
deleteDashboardArgs: []struct {
orgId int64
dashboardId int64
orgId int64
dashboardId int64
dashboardUID string
}{},
}

@ -326,7 +326,7 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
dashService, err := service.ProvideDashboardServiceImpl(
cfg, dashboardStoreService, folderStore,
featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(), dashPermissionService, ac,
foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil,
foldertest.NewFakeService(), folder.NewFakeStore(), nil, zanzana.NewNoopClient(), nil, nil,
)
require.NoError(t, err)

Loading…
Cancel
Save