K8s: Remove kubernetesClientDashboardsFolders feature flag (#108626)

This commit is contained in:
Stephanie Hingtgen
2025-07-29 16:52:57 -05:00
committed by GitHub
parent e3cb84bef8
commit 1f025fe1a3
69 changed files with 1585 additions and 3235 deletions
@@ -872,11 +872,9 @@ func setUpServiceTest(t *testing.T, withDashboardMock bool, cfgOverrides ...conf
rr := routing.NewRouteRegister()
tracer := tracing.InitializeTracerForTest()
fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder"}
mockFolder := &foldertest.FakeService{
ExpectedFolders: []*folder.Folder{fakeFolder},
ExpectedFolder: fakeFolder,
}
fakeFolder := &folder.Folder{UID: "folderUID", Title: "Folder", Fullpath: "Folder"}
mockFolder := foldertest.NewFakeService()
mockFolder.AddFolder(fakeFolder)
cfg := setting.NewCfg()
section, err := cfg.Raw.NewSection("cloud_migration")
@@ -3,6 +3,7 @@ package cloudmigrationimpl
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
@@ -18,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -408,6 +410,9 @@ func updateNotificationPolicyTree(t *testing.T, ctx context.Context, service *Se
func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, isPaused bool, ruleGroup string) models.AlertRule {
t.Helper()
// Ensure the folder exists before creating alert rules
createFolder(t, ctx, service, user, "folderUID", "Test Folder")
rule := models.AlertRule{
OrgID: user.GetOrgID(),
Title: fmt.Sprintf("Alert Rule SLO (Paused: %v) - %v", isPaused, ruleGroup),
@@ -437,6 +442,19 @@ func createAlertRule(t *testing.T, ctx context.Context, service *Service, user *
return createdRule
}
func createFolder(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, uid, title string) {
t.Helper()
_, err := service.folderService.Create(ctx, &folder.CreateFolderCommand{
OrgID: user.GetOrgID(),
UID: uid,
Title: title,
SignedInUser: user,
})
if err != nil && !errors.Is(err, dashboards.ErrFolderWithSameUIDExists) {
require.NoError(t, err)
}
}
func createAlertRuleGroup(t *testing.T, ctx context.Context, service *Service, user *user.SignedInUser, title string, rules []models.AlertRule) models.AlertRuleGroup {
t.Helper()
@@ -146,14 +146,6 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.DashboardImport).Inc()
// in the k8s flow, we connect the library panels in pkg/registry/apis/dashboard/legacy/sql_dashboards.go
if !s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
err = s.libraryPanelService.ConnectLibraryPanelsForDashboard(ctx, req.User, savedDashboard)
if err != nil {
return nil, err
}
}
revision := savedDashboard.Data.Get("revision").MustInt64(0)
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.DashboardImport).Inc()
return &dashboardimport.ImportDashboardResponse{
@@ -46,16 +46,11 @@ func TestImportDashboardService(t *testing.T) {
}
importLibraryPanelsForDashboard := false
connectLibraryPanelsForDashboardCalled := false
libraryPanelService := &libraryPanelServiceMock{
importLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, libraryPanels *simplejson.Json, panels []any, folderID int64, folderUID string) error {
importLibraryPanelsForDashboard = true
return nil
},
connectLibraryPanelsForDashboardFunc: func(ctx context.Context, signedInUser identity.Requester, dash *dashboards.Dashboard) error {
connectLibraryPanelsForDashboardCalled = true
return nil
},
}
folderService := &foldertest.FakeService{
ExpectedFolder: &folder.Folder{
@@ -98,7 +93,6 @@ func TestImportDashboardService(t *testing.T) {
require.Equal(t, "prom", panel.Get("datasource").MustString())
require.True(t, importLibraryPanelsForDashboard)
require.True(t, connectLibraryPanelsForDashboardCalled)
})
t.Run("When importing a non-plugin dashboard should save dashboard and sync library panels", func(t *testing.T) {
-9
View File
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
@@ -77,12 +76,8 @@ type DashboardProvisioningService interface {
type Store interface {
DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error
CleanupAfterDelete(ctx context.Context, cmd *DeleteDashboardCommand) error
DeleteAllDashboards(ctx context.Context, orgID int64) error
DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error
FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error)
GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error)
GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error)
GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error)
// GetDashboardsByPluginID retrieves dashboards identified by plugin.
GetDashboardsByPluginID(ctx context.Context, query *GetDashboardsByPluginIDQuery) ([]*Dashboard, error)
GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error)
@@ -97,11 +92,7 @@ type Store interface {
// ValidateDashboardBeforeSave validates a dashboard before save.
ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error)
Count(context.Context, *quota.ScopeParameters) (*quota.Map, error)
CountInOrg(ctx context.Context, orgID int64, isFolder bool) (int64, error)
// CountDashboardsInFolder returns the number of dashboards associated with
// the given parent folder ID.
CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error)
DeleteDashboardsInFolders(ctx context.Context, request *DeleteDashboardsInFolderRequest) error
GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error)
@@ -2,10 +2,8 @@ package database
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"go.opentelemetry.io/otel"
@@ -25,7 +23,6 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/migrations"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/star"
"github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/tag"
"github.com/grafana/grafana/pkg/setting"
@@ -352,34 +349,6 @@ func (d *dashboardStore) UnprovisionDashboard(ctx context.Context, id int64) err
})
}
func (d *dashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteOrphanedProvisionedDashboards")
defer span.End()
return d.store.WithDbSession(ctx, func(sess *db.Session) error {
var result []*dashboards.DashboardProvisioning
convertedReaderNames := make([]any, len(cmd.ReaderNames))
for index, readerName := range cmd.ReaderNames {
convertedReaderNames[index] = readerName
}
err := sess.NotIn("name", convertedReaderNames...).Find(&result)
if err != nil {
return err
}
for _, deleteDashCommand := range result {
err := d.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{ID: deleteDashCommand.DashboardID})
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
return err
}
}
return nil
})
}
func (d *dashboardStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.Count")
defer span.End()
@@ -690,16 +659,6 @@ func (d *dashboardStore) CleanupAfterDelete(ctx context.Context, cmd *dashboards
return err
}
func (d *dashboardStore) DeleteAllDashboards(ctx context.Context, orgID int64) error {
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteAllDashboards")
defer span.End()
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Where("org_id = ?", orgID).Delete(&dashboards.Dashboard{})
return err
})
}
// FIXME: Remove me and handle nested deletions in the service with the DashboardPermissionsService
func (d *dashboardStore) deleteResourcePermissions(sess *db.Session, orgID int64, resourceScope string) error {
// retrieve all permissions for the resource scope and org id
@@ -821,58 +780,6 @@ func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.Get
return queryResult, err
}
func (d *dashboardStore) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboardUIDByID")
defer span.End()
us := &dashboards.DashboardRef{}
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
var rawSQL = `SELECT uid, slug, folder_uid from dashboard WHERE Id=?`
exists, err := sess.SQL(rawSQL, query.ID).Get(us)
if err != nil {
return err
} else if !exists {
return dashboards.ErrDashboardNotFound
}
return nil
})
if err != nil {
return nil, err
}
return us, nil
}
func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.GetDashboards")
defer span.End()
var dashboards = make([]*dashboards.Dashboard, 0)
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 {
return star.ErrCommandValidationFailed
}
// remove soft deleted dashboards from the response
sess.Where("deleted IS NULL")
if len(query.DashboardIDs) > 0 {
sess.In("id", query.DashboardIDs)
} else {
sess.In("uid", query.DashboardUIDs)
}
if query.OrgID > 0 {
sess.Where("org_id = ?", query.OrgID)
}
err := sess.Find(&dashboards)
return err
})
if err != nil {
return nil, err
}
return dashboards, nil
}
func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) ([]dashboards.DashboardSearchProjection, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.FindDashboards")
defer span.End()
@@ -1025,39 +932,6 @@ func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards
return queryResult, nil
}
// CountDashboardsInFolder returns a count of all dashboards associated with the
// given parent folder ID.
func (d *dashboardStore) CountDashboardsInFolders(
ctx context.Context, req *dashboards.CountDashboardsInFolderRequest) (int64, error) {
ctx, span := tracer.Start(ctx, "dashboards.database.CountDashboardsInFolders")
defer span.End()
if len(req.FolderUIDs) == 0 {
return 0, nil
}
var count int64
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
s := strings.Builder{}
args := make([]any, 0, 3)
s.WriteString("SELECT COUNT(*) FROM dashboard WHERE ")
if len(req.FolderUIDs) == 1 && req.FolderUIDs[0] == "" {
s.WriteString("folder_uid IS NULL")
} else {
s.WriteString(fmt.Sprintf("folder_uid IN (%s)", strings.Repeat("?,", len(req.FolderUIDs)-1)+"?"))
for _, folderUID := range req.FolderUIDs {
args = append(args, folderUID)
}
}
s.WriteString(" AND org_id = ? AND is_folder = ? AND deleted IS NULL")
args = append(args, req.OrgID, d.store.GetDialect().BooleanValue(false))
sql := s.String()
_, err := sess.SQL(sql, args...).Get(&count)
return err
})
return count, err
}
func (d *dashboardStore) DeleteDashboardsInFolders(
ctx context.Context, req *dashboards.DeleteDashboardsInFolderRequest) error {
ctx, span := tracer.Start(ctx, "dashboards.database.DeleteDashboardsInFolders")
@@ -59,42 +59,6 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) {
require.NotEqual(t, 0, dash.ID)
dashId := dash.ID
t.Run("Deleting orphaned provisioned dashboards", func(t *testing.T) {
saveCmd := dashboards.SaveDashboardCommand{
OrgID: 1,
IsFolder: false,
FolderUID: dash.UID,
Dashboard: simplejson.NewFromAny(map[string]any{
"id": nil,
"title": "another_dashboard",
}),
}
provisioning := &dashboards.DashboardProvisioning{
Name: "another_reader",
ExternalID: "/var/grafana.json",
Updated: now.Unix(),
}
anotherDash, err := dashboardStore.SaveProvisionedDashboard(context.Background(), saveCmd, provisioning)
require.Nil(t, err)
query := &dashboards.GetDashboardsQuery{DashboardIDs: []int64{anotherDash.ID}}
queryResult, err := dashboardStore.GetDashboards(context.Background(), query)
require.Nil(t, err)
require.NotNil(t, queryResult)
deleteCmd := &dashboards.DeleteOrphanedProvisionedDashboardsCommand{ReaderNames: []string{"default"}}
require.Nil(t, dashboardStore.DeleteOrphanedProvisionedDashboards(context.Background(), deleteCmd))
query = &dashboards.GetDashboardsQuery{DashboardIDs: []int64{dash.ID, anotherDash.ID}}
queryResult, err = dashboardStore.GetDashboards(context.Background(), query)
require.Nil(t, err)
require.Equal(t, 1, len(queryResult))
require.Equal(t, dashId, queryResult[0].ID)
})
t.Run("Can query for provisioned dashboards", func(t *testing.T) {
rslt, err := dashboardStore.GetProvisionedDashboardData(context.Background(), "default")
require.Nil(t, err)
+60 -129
View File
@@ -10,27 +10,19 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
libmodel "github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
)
@@ -185,15 +177,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
require.False(t, queryResult.IsFolder)
})
t.Run("Should be able to get a dashboard UID by ID", func(t *testing.T) {
setup()
query := dashboards.GetDashboardRefByIDQuery{ID: savedDash.ID}
queryResult, err := dashboardStore.GetDashboardUIDByID(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, queryResult.UID, savedDash.UID)
require.Equal(t, queryResult.FolderUID, savedFolder.UID)
})
t.Run("Shouldn't be able to get a dashboard with just an OrgID", func(t *testing.T) {
setup()
query := dashboards.GetDashboardQuery{
@@ -216,19 +199,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
require.Error(t, err, dashboards.ErrDashboardNotFound)
})
t.Run("Should be able to get dashboards by IDs & UIDs", func(t *testing.T) {
setup()
query := dashboards.GetDashboardsQuery{DashboardIDs: []int64{savedDash.ID, savedDash2.ID}}
queryResult, err := dashboardStore.GetDashboards(context.Background(), &query)
require.NoError(t, err)
assert.Equal(t, len(queryResult), 2)
query = dashboards.GetDashboardsQuery{DashboardUIDs: []string{savedDash.UID, savedDash2.UID}}
queryResult, err = dashboardStore.GetDashboards(context.Background(), &query)
require.NoError(t, err)
assert.Equal(t, len(queryResult), 2)
})
t.Run("Should be able to delete dashboard and associated tags", func(t *testing.T) {
setup()
dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this")
@@ -330,32 +300,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
require.Len(t, res, 0)
})
t.Run("Should be able to delete all dashboards for an org", func(t *testing.T) {
setup()
dash1 := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this")
dash2 := insertTestDashboard(t, dashboardStore, "delete me2", 1, 0, "", false, "delete this2")
dash3 := insertTestDashboard(t, dashboardStore, "dont delete me", 2, 0, "", false, "dont delete me")
err := dashboardStore.DeleteAllDashboards(context.Background(), 1)
require.NoError(t, err)
// no dashboards should exist for org 1
queryResult, err := dashboardStore.GetDashboards(context.Background(), &dashboards.GetDashboardsQuery{
OrgID: 1,
DashboardUIDs: []string{dash1.UID, dash2.UID},
})
require.NoError(t, err)
assert.Equal(t, len(queryResult), 0)
// but we should still have one for org 2
queryResult, err = dashboardStore.GetDashboards(context.Background(), &dashboards.GetDashboardsQuery{
OrgID: 2,
DashboardUIDs: []string{dash3.UID},
})
require.NoError(t, err)
assert.Equal(t, len(queryResult), 1)
})
t.Run("Should be able to get all dashboards for an org", func(t *testing.T) {
setup()
dash1 := insertTestDashboard(t, dashboardStore, "org3test1", 3, 0, "", false, "org 1 test 1")
@@ -679,22 +623,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
require.Equal(t, len(hit2.Tags), 1)
})
t.Run("Can count dashboards by parent folder", func(t *testing.T) {
setup()
// setup() saves one dashboard in the general folder and two in the "savedFolder".
count, err := dashboardStore.CountDashboardsInFolders(
context.Background(),
&dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{""}, OrgID: 1})
require.NoError(t, err)
require.Equal(t, int64(1), count)
count, err = dashboardStore.CountDashboardsInFolders(
context.Background(),
&dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
require.Equal(t, int64(2), count)
})
t.Run("Can delete dashboards in folder", func(t *testing.T) {
setup()
folder := insertTestDashboard(t, dashboardStore, "dash folder", 1, 0, "", true, "prod", "webapp")
@@ -703,10 +631,6 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
err := dashboardStore.DeleteDashboardsInFolders(context.Background(), &dashboards.DeleteDashboardsInFolderRequest{OrgID: folder.OrgID, FolderUIDs: []string{folder.UID}})
require.NoError(t, err)
count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1})
require.NoError(t, err)
require.Equal(t, count, int64(0))
})
}
@@ -830,6 +754,59 @@ func TestIntegrationDashboard_Filter(t *testing.T) {
assert.Equal(t, dashB.ID, results[0].ID)
}
// has to be added to both the folder & the dashboard table
func insertTestFolder(t *testing.T, dashboardStore dashboards.Store, sqlStore db.DB, title string, orgId int64, parentUID string, tags ...interface{}) *dashboards.Dashboard {
t.Helper()
cmd := dashboards.SaveDashboardCommand{
OrgID: orgId,
FolderUID: parentUID,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": title,
"tags": tags,
}),
}
dash, err := dashboardStore.SaveDashboard(context.Background(), cmd)
require.NoError(t, err)
require.NotNil(t, dash)
dash.Data.Set("id", dash.ID)
dash.Data.Set("uid", dash.UID)
err = sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error {
type folder struct {
ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
UID string `xorm:"uid"`
ParentUID *string `xorm:"parent_uid"`
Title string `xorm:"title"`
Description string `xorm:"description"`
Created time.Time `xorm:"created"`
Updated time.Time `xorm:"updated"`
}
f := &folder{
OrgID: orgId,
UID: dash.UID,
Title: title,
Description: "",
Created: time.Now(),
Updated: time.Now(),
}
if parentUID != "" {
f.ParentUID = &parentUID
}
_, err := sess.Insert(f)
return err
})
require.NoError(t, err)
return dash
}
func TestIntegrationFindDashboardsByTitle(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -844,13 +821,6 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
orgID := int64(1)
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false, []string{"tag1", "tag2"})
ac := acimpl.ProvideAccessControl(features)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
user := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
@@ -863,23 +833,10 @@ func TestIntegrationFindDashboardsByTitle(t *testing.T) {
},
}
f0, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: "f0",
SignedInUser: user,
})
require.NoError(t, err)
f0 := insertTestFolder(t, dashboardStore, sqlStore, "f0", orgID, "")
insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, 0, f0.UID, false, []string{"tag3"})
subfolder, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: "subfolder",
ParentUID: f0.UID,
SignedInUser: user,
})
require.NoError(t, err)
subfolder := insertTestFolder(t, dashboardStore, sqlStore, "subfolder", orgID, f0.UID)
insertTestDashboard(t, dashboardStore, "dashboard under subfolder", orgID, 0, subfolder.UID, false)
type res struct {
@@ -981,14 +938,6 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
orgID := int64(1)
insertTestDashboard(t, dashboardStore, "dashboard under general", orgID, 0, "", false)
ac := acimpl.ProvideAccessControl(features)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderServiceWithFlagOn := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
user := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
@@ -1001,31 +950,13 @@ func TestIntegrationFindDashboardsByFolder(t *testing.T) {
},
}
f0, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: "f0",
SignedInUser: user,
})
require.NoError(t, err)
// nolint:staticcheck
insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, f0.ID, f0.UID, false)
f0 := insertTestFolder(t, dashboardStore, sqlStore, "f0", orgID, "")
insertTestDashboard(t, dashboardStore, "dashboard under f0", orgID, 0, f0.UID, false)
f1, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: "f1",
SignedInUser: user,
})
require.NoError(t, err)
// nolint:staticcheck
insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, f1.ID, f1.UID, false)
f1 := insertTestFolder(t, dashboardStore, sqlStore, "f1", orgID, "")
insertTestDashboard(t, dashboardStore, "dashboard under f1", orgID, 0, f1.UID, false)
subfolder, err := folderServiceWithFlagOn.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: orgID,
Title: "subfolder",
ParentUID: f0.UID,
SignedInUser: user,
})
require.NoError(t, err)
subfolder := insertTestFolder(t, dashboardStore, sqlStore, "subfolder", orgID, f0.UID)
type res struct {
title string
@@ -150,10 +150,6 @@ func (dr *DashboardServiceImpl) cleanupK8sDashboardResources(ctx context.Context
ctx, span := tracer.Start(ctx, "dashboards.service.cleanupK8sDashboardResources")
defer span.End()
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return nil
}
readingFromLegacy := dualwrite.IsReadingLegacyDashboardsAndFolders(ctx, dr.dual)
if readingFromLegacy {
// Legacy does its own cleanup
@@ -444,45 +440,41 @@ func (dr *DashboardServiceImpl) getPermissionsService(isFolder bool) accesscontr
}
func (dr *DashboardServiceImpl) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
u := &quota.Map{}
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return u, err
}
u := &quota.Map{}
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return u, err
}
total := int64(0)
for _, org := range orgs {
ctx, _ := identity.WithServiceIdentity(ctx, org.ID)
orgDashboards, err := dr.CountDashboardsInOrg(ctx, org.ID)
if err != nil {
return nil, err
}
total += orgDashboards
if scopeParams != nil && scopeParams.OrgID == org.ID {
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
if err != nil {
return nil, err
}
u.Set(tag, orgDashboards)
}
}
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
total := int64(0)
for _, org := range orgs {
ctx, _ := identity.WithServiceIdentity(ctx, org.ID)
orgDashboards, err := dr.CountDashboardsInOrg(ctx, org.ID)
if err != nil {
return nil, err
}
u.Set(tag, total)
total += orgDashboards
return u, nil
if scopeParams != nil && scopeParams.OrgID == org.ID {
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
if err != nil {
return nil, err
}
u.Set(tag, orgDashboards)
}
}
return dr.dashboardStore.Count(ctx, scopeParams)
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
if err != nil {
return nil, err
}
u.Set(tag, total)
return u, nil
}
func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*dashboards.DashboardRef, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesLibraryPanelConnections) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesLibraryPanelConnections) {
res, err := dr.k8sclient.Search(ctx, orgID, &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
Fields: []*resourcepb.Requirement{
@@ -519,20 +511,16 @@ func (dr *DashboardServiceImpl) GetDashboardsByLibraryPanelUID(ctx context.Conte
}
func (dr *DashboardServiceImpl) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
resp, err := dr.k8sclient.GetStats(ctx, orgID)
if err != nil {
return 0, err
}
if len(resp.Stats) != 1 {
return 0, fmt.Errorf("expected 1 stat, got %d", len(resp.Stats))
}
return resp.Stats[0].Count, nil
resp, err := dr.k8sclient.GetStats(ctx, orgID)
if err != nil {
return 0, err
}
return dr.dashboardStore.CountInOrg(ctx, orgID, false)
if len(resp.Stats) != 1 {
return 0, fmt.Errorf("expected 1 stat, got %d", len(resp.Stats))
}
return resp.Stats[0].Count, nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
@@ -557,93 +545,46 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
}
func (dr *DashboardServiceImpl) GetProvisionedDashboardData(ctx context.Context, name string) ([]*dashboards.DashboardProvisioning, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return nil, err
}
results := []*dashboards.DashboardProvisioning{}
for _, org := range orgs {
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck
ManagerIdentity: name,
OrgId: org.ID,
})
if err != nil {
return nil, err
}
for _, r := range res {
results = append(results, &r.DashboardProvisioning)
}
}
return results, nil
}
return dr.dashboardStore.GetProvisionedDashboardData(ctx, name)
}
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
// if dashboard id is 0, it is a new dashboard
if dashboardID == 0 {
return nil, nil
}
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return nil, err
}
for _, org := range orgs {
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck
OrgId: org.ID,
DashboardIds: []int64{dashboardID},
})
if err != nil {
return nil, err
}
if len(res) == 1 {
return &res[0].DashboardProvisioning, nil
} else if len(res) > 1 {
return nil, fmt.Errorf("found more than one provisioned dashboard with ID %d", dashboardID)
}
}
return nil, nil
}
data, err := dr.dashboardStore.GetProvisionedDataByDashboardID(ctx, dashboardID)
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return nil, err
}
if data == nil {
results := []*dashboards.DashboardProvisioning{}
for _, org := range orgs {
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck
ManagerIdentity: name,
OrgId: org.ID,
})
if err != nil {
return nil, err
}
for _, r := range res {
results = append(results, &r.DashboardProvisioning)
}
}
return results, nil
}
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardID(ctx context.Context, dashboardID int64) (*dashboards.DashboardProvisioning, error) {
// if dashboard id is 0, it is a new dashboard
if dashboardID == 0 {
return nil, nil
}
return &dashboards.DashboardProvisioning{
DashboardID: data.Dashboard.ID,
Name: data.Provisioner,
ExternalID: data.ExternalID,
CheckSum: data.CheckSum,
Updated: data.ProvisionUpdate,
}, nil
}
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
if dashboardUID == "" {
return nil, nil
}
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return nil, err
}
for _, org := range orgs {
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck
OrgId: orgID,
DashboardUIDs: []string{dashboardUID},
ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck
OrgId: org.ID,
DashboardIds: []int64{dashboardID},
})
if err != nil {
return nil, err
@@ -652,27 +593,34 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co
if len(res) == 1 {
return &res[0].DashboardProvisioning, nil
} else if len(res) > 1 {
return nil, fmt.Errorf("found more than one provisioned dashboard with UID %s", dashboardUID)
return nil, fmt.Errorf("found more than one provisioned dashboard with ID %d", dashboardID)
}
}
return nil, nil
}
func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx context.Context, orgID int64, dashboardUID string) (*dashboards.DashboardProvisioning, error) {
if dashboardUID == "" {
return nil, nil
}
data, err := dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID)
res, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, // nolint:staticcheck
OrgId: orgID,
DashboardUIDs: []string{dashboardUID},
})
if err != nil {
return nil, err
}
if data == nil {
return nil, nil
if len(res) == 1 {
return &res[0].DashboardProvisioning, nil
} else if len(res) > 1 {
return nil, fmt.Errorf("found more than one provisioned dashboard with UID %s", dashboardUID)
}
return &dashboards.DashboardProvisioning{
DashboardID: data.Dashboard.ID,
Name: data.Provisioner,
ExternalID: data.ExternalID,
CheckSum: data.CheckSum,
Updated: data.ProvisionUpdate,
}, nil
return nil, nil
}
func (dr *DashboardServiceImpl) ValidateBasicDashboardProperties(title string, uid string, message string) error {
@@ -730,7 +678,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
}
// Validate folder
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && (dash.FolderID != 0 || dash.FolderUID != "") { // nolint:staticcheck
if dash.FolderID != 0 || dash.FolderUID != "" { // nolint:staticcheck
folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{
OrgID: dash.OrgID,
UID: &dash.FolderUID,
@@ -744,22 +692,6 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
// nolint:staticcheck
dash.FolderID = folder.ID
dash.FolderUID = folder.UID
} else if dash.FolderUID != "" {
folder, err := dr.folderStore.GetFolderByUID(ctx, dash.OrgID, dash.FolderUID)
if err != nil {
return nil, err
}
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck
dash.FolderID = folder.ID
} else if dash.FolderID != 0 { // nolint:staticcheck
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
// nolint:staticcheck
folder, err := dr.folderStore.GetFolderByID(ctx, dash.OrgID, dash.FolderID)
if err != nil {
return nil, err
}
dash.FolderUID = folder.UID
}
isParentFolderChanged, err := dr.ValidateDashboardBeforeSave(ctx, dash, dto.Overwrite)
@@ -944,46 +876,42 @@ func (dr *DashboardServiceImpl) waitForSearchQuery(ctx context.Context, query *d
}
func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
// check each org for orphaned provisioned dashboards
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
// check each org for orphaned provisioned dashboards
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return err
}
for _, org := range orgs {
ctx, _ := identity.WithServiceIdentity(ctx, org.ID)
// find all dashboards in the org that have a file repo set that is not in the given readers list
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck
ManagerIdentityNotIn: cmd.ReaderNames,
OrgId: org.ID,
})
if err != nil {
return err
}
dr.log.Debug("Found dashboards to be deleted", "orgId", org.ID, "count", len(foundDashs))
for _, org := range orgs {
ctx, _ := identity.WithServiceIdentity(ctx, org.ID)
// find all dashboards in the org that have a file repo set that is not in the given readers list
foundDashs, err := dr.searchProvisionedDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
ManagedBy: utils.ManagerKindClassicFP, //nolint:staticcheck
ManagerIdentityNotIn: cmd.ReaderNames,
OrgId: org.ID,
})
// delete them
var deletedUids []string
for _, foundDash := range foundDashs {
if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil {
return err
}
deletedUids = append(deletedUids, foundDash.DashboardUID)
}
if len(deletedUids) > 0 {
// wait for deleted dashboards to be removed from the index
err = dr.waitForSearchQuery(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: org.ID, DashboardUIDs: deletedUids}, 5, 0)
if err != nil {
return err
}
dr.log.Debug("Found dashboards to be deleted", "orgId", org.ID, "count", len(foundDashs))
// delete them
var deletedUids []string
for _, foundDash := range foundDashs {
if err = dr.deleteDashboard(ctx, foundDash.DashboardID, foundDash.DashboardUID, org.ID, false); err != nil {
return err
}
deletedUids = append(deletedUids, foundDash.DashboardUID)
}
if len(deletedUids) > 0 {
// wait for deleted dashboards to be removed from the index
err = dr.waitForSearchQuery(ctx, &dashboards.FindPersistedDashboardsQuery{OrgId: org.ID, DashboardUIDs: deletedUids}, 5, 0)
if err != nil {
return err
}
}
}
return nil
}
return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd)
return nil
}
func (dr *DashboardServiceImpl) ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error {
@@ -1034,13 +962,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
return nil, fmt.Errorf("failed to build save dashboard command. cmd is nil")
}
var dash *dashboards.Dashboard
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
dash, err = dr.saveProvisionedDashboardThroughK8s(ctx, cmd, provisioning, false)
} else {
dash, err = dr.dashboardStore.SaveProvisionedDashboard(ctx, *cmd, provisioning)
}
dash, err := dr.saveProvisionedDashboardThroughK8s(ctx, cmd, provisioning, false)
if err != nil {
return nil, err
}
@@ -1065,10 +987,6 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
return nil, err
}
// Only set default permissions if the Folder API Server is disabled.
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
dr.setDefaultFolderPermissions(ctx, dto, f)
}
return f, nil
}
@@ -1103,11 +1021,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
}
func (dr *DashboardServiceImpl) saveDashboard(ctx context.Context, cmd *dashboards.SaveDashboardCommand) (*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID)
}
return dr.dashboardStore.SaveDashboard(ctx, *cmd)
return dr.saveDashboardThroughK8s(ctx, cmd, cmd.OrgID)
}
// DeleteDashboard removes dashboard from the DB. Errors out if the dashboard was provisioned. Should be used for
@@ -1118,11 +1032,7 @@ func (dr *DashboardServiceImpl) DeleteDashboard(ctx context.Context, dashboardId
// DeleteAllDashboards will delete all dashboards within a given org.
func (dr *DashboardServiceImpl) DeleteAllDashboards(ctx context.Context, orgId int64) error {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return dr.deleteAllDashboardThroughK8s(ctx, orgId)
}
return dr.dashboardStore.DeleteAllDashboards(ctx, orgId)
return dr.deleteAllDashboardThroughK8s(ctx, orgId)
}
func (dr *DashboardServiceImpl) GetDashboardByPublicUid(ctx context.Context, dashboardPublicUid string) (*dashboards.Dashboard, error) {
@@ -1141,28 +1051,7 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID}
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard)
}
if validateProvisionedDashboard {
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId)
if err != nil {
return fmt.Errorf("%v: %w", "failed to check if dashboard is provisioned", err)
}
if provisionedData != nil {
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
}
}
// deletes all related public dashboard entities
err := dr.publicDashboardService.DeleteByDashboardUIDs(ctx, orgId, []string{dashboardUID})
if err != nil {
return err
}
return dr.dashboardStore.DeleteDashboard(ctx, cmd)
return dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard)
}
func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashboards.SaveDashboardDTO) (
@@ -1195,65 +1084,58 @@ 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 {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
orgs, err := dr.orgService.Search(ctx, &org.SearchOrgsQuery{})
if err != nil {
return err
}
for _, org := range orgs {
ctx, _ = identity.WithServiceIdentity(ctx, org.ID)
dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId})
if err != nil {
// if we can't find it in this org, try the next one
continue
}
_, err = dr.saveProvisionedDashboardThroughK8s(ctx, &dashboards.SaveDashboardCommand{
OrgID: org.ID,
PluginID: dash.PluginID,
FolderUID: dash.FolderUID,
FolderID: dash.FolderID, // nolint:staticcheck
UpdatedAt: time.Now(),
Dashboard: dash.Data,
}, nil, true)
if err != nil {
return err
}
for _, org := range orgs {
ctx, _ = identity.WithServiceIdentity(ctx, org.ID)
dash, err := dr.getDashboardThroughK8s(ctx, &dashboards.GetDashboardQuery{OrgID: org.ID, ID: dashboardId})
if err != nil {
// if we can't find it in this org, try the next one
continue
}
_, err = dr.saveProvisionedDashboardThroughK8s(ctx, &dashboards.SaveDashboardCommand{
OrgID: org.ID,
PluginID: dash.PluginID,
FolderUID: dash.FolderUID,
FolderID: dash.FolderID, // nolint:staticcheck
UpdatedAt: time.Now(),
Dashboard: dash.Data,
}, nil, true)
if err != nil {
return err
}
return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId)
}
return dashboards.ErrDashboardNotFound
return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId)
}
return dr.dashboardStore.UnprovisionDashboard(ctx, dashboardId)
return dashboards.ErrDashboardNotFound
}
func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, query *dashboards.GetDashboardsByPluginIDQuery) ([]*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: query.OrgID,
ManagedBy: utils.ManagerKindPlugin,
ManagerIdentity: query.PluginID,
})
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: query.OrgID,
ManagedBy: utils.ManagerKindPlugin,
ManagerIdentity: query.PluginID,
})
if err != nil {
return nil, err
}
// search only returns the metadata, need to get the dashboard.Data too
results := make([]*dashboards.Dashboard, len(dashs))
for i, d := range dashs {
dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID})
if err != nil {
return nil, err
}
// search only returns the metadata, need to get the dashboard.Data too
results := make([]*dashboards.Dashboard, len(dashs))
for i, d := range dashs {
dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID})
if err != nil {
return nil, err
}
results[i] = dash
}
return results, nil
results[i] = dash
}
return dr.dashboardStore.GetDashboardsByPluginID(ctx, query)
return results, nil
}
// (sometimes) called by the k8s storage engine after creating an object
@@ -1361,92 +1243,62 @@ func (dr *DashboardServiceImpl) SetDefaultPermissions(ctx context.Context, dto *
}
}
func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, cmd *folder.CreateFolderCommand, f *folder.Folder) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) || !dr.cfg.RBAC.PermissionsOnCreation("folder") || f.ParentUID != "" {
return
}
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultFolderPermissions")
defer span.End()
permissions := []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
}
if _, err := dr.folderPermissions.SetPermissions(ctx, cmd.OrgID, f.UID, permissions...); err != nil {
dr.log.Error("Could not set default folder permissions", "folder", f.Title, "error", err)
}
}
func (dr *DashboardServiceImpl) GetDashboard(ctx context.Context, query *dashboards.GetDashboardQuery) (*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return dr.getDashboardThroughK8s(ctx, query)
}
return dr.dashboardStore.GetDashboard(ctx, query)
return dr.getDashboardThroughK8s(ctx, query)
}
func (dr *DashboardServiceImpl) GetDashboardUIDByID(ctx context.Context, query *dashboards.GetDashboardRefByIDQuery) (*dashboards.DashboardRef, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: requester.GetOrgID(),
DashboardIds: []int64{query.ID},
})
if err != nil {
return nil, err
}
if len(result) == 0 {
return nil, dashboards.ErrDashboardNotFound
} else if len(result) > 1 {
return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result))
}
return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug, FolderUID: result[0].FolderUID}, nil
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
result, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: requester.GetOrgID(),
DashboardIds: []int64{query.ID},
})
if err != nil {
return nil, err
}
return dr.dashboardStore.GetDashboardUIDByID(ctx, query)
if len(result) == 0 {
return nil, dashboards.ErrDashboardNotFound
} else if len(result) > 1 {
return nil, fmt.Errorf("unexpected number of dashboards found: %d. desired: 1", len(result))
}
return &dashboards.DashboardRef{UID: result[0].UID, Slug: result[0].Slug, FolderUID: result[0].FolderUID}, nil
}
// expensive query in new flow !! use sparingly - only if you truly need dashboard.Data
func (dr *DashboardServiceImpl) GetDashboards(ctx context.Context, query *dashboards.GetDashboardsQuery) ([]*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
if query.OrgID == 0 {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
query.OrgID = requester.GetOrgID()
}
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
DashboardIds: query.DashboardIDs,
OrgId: query.OrgID,
DashboardUIDs: query.DashboardUIDs,
})
if query.OrgID == 0 {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
// search only returns the metadata, need to get the dashboard.Data too
results := make([]*dashboards.Dashboard, len(dashs))
for i, d := range dashs {
dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID})
if err != nil {
return nil, err
}
results[i] = dash
}
return results, nil
query.OrgID = requester.GetOrgID()
}
return dr.dashboardStore.GetDashboards(ctx, query)
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
DashboardIds: query.DashboardIDs,
OrgId: query.OrgID,
DashboardUIDs: query.DashboardUIDs,
})
if err != nil {
return nil, err
}
// search only returns the metadata, need to get the dashboard.Data too
results := make([]*dashboards.Dashboard, len(dashs))
for i, d := range dashs {
dash, err := dr.GetDashboard(ctx, &dashboards.GetDashboardQuery{OrgID: d.OrgID, UID: d.UID})
if err != nil {
return nil, err
}
results[i] = dash
}
return results, nil
}
func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context, user identity.Requester) ([]*dashboards.DashboardRef, error) {
@@ -1468,21 +1320,10 @@ func (dr *DashboardServiceImpl) getDashboardsSharedWithUser(ctx context.Context,
return []*dashboards.DashboardRef{}, nil
}
dashboardsQuery := &dashboards.GetDashboardsQuery{
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
DashboardUIDs: dashboardUids,
OrgID: user.GetOrgID(),
}
var err error
var dashs []*dashboards.Dashboard
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
dashs, err = dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
DashboardUIDs: dashboardUids,
OrgId: user.GetOrgID(),
})
} else {
dashs, err = dr.dashboardStore.GetDashboards(ctx, dashboardsQuery)
}
OrgId: user.GetOrgID(),
})
if err != nil {
return nil, err
}
@@ -1573,67 +1414,63 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb
}(time.Now())
}
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesClientDashboardsFolders) {
if query.OrgId == 0 {
requester, err := identity.GetRequester(ctx)
if query.OrgId == 0 {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
query.OrgId = requester.GetOrgID()
}
response, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
if err != nil {
return nil, err
}
folderNames, err := dr.fetchFolderNames(ctx, query, response.Hits)
if err != nil {
return nil, err
}
finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits))
for i, hit := range response.Hits {
folderTitle := ""
folderID := int64(0)
if f, ok := folderNames[hit.Folder]; ok {
folderTitle = f.Title
folderID = f.ID
}
result := dashboards.DashboardSearchProjection{
ID: hit.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID),
UID: hit.Name,
OrgID: query.OrgId,
Title: hit.Title,
Slug: slugify.Slugify(hit.Title),
IsFolder: false,
FolderUID: hit.Folder,
FolderTitle: folderTitle,
FolderID: folderID,
FolderSlug: slugify.Slugify(folderTitle),
Tags: hit.Tags,
}
if hit.Field != nil && query.Sort.Name != "" {
fieldName, _, err := legacysearcher.ParseSortName(query.Sort.Name)
if err != nil {
return nil, err
}
query.OrgId = requester.GetOrgID()
result.SortMeta = hit.Field.GetNestedInt64(fieldName)
}
response, err := dr.searchDashboardsThroughK8sRaw(ctx, query)
if err != nil {
return nil, err
if hit.Resource == folderv1.RESOURCE {
result.IsFolder = true
}
folderNames, err := dr.fetchFolderNames(ctx, query, response.Hits)
if err != nil {
return nil, err
}
finalResults := make([]dashboards.DashboardSearchProjection, len(response.Hits))
for i, hit := range response.Hits {
folderTitle := ""
folderID := int64(0)
if f, ok := folderNames[hit.Folder]; ok {
folderTitle = f.Title
folderID = f.ID
}
result := dashboards.DashboardSearchProjection{
ID: hit.Field.GetNestedInt64(resource.SEARCH_FIELD_LEGACY_ID),
UID: hit.Name,
OrgID: query.OrgId,
Title: hit.Title,
Slug: slugify.Slugify(hit.Title),
IsFolder: false,
FolderUID: hit.Folder,
FolderTitle: folderTitle,
FolderID: folderID,
FolderSlug: slugify.Slugify(folderTitle),
Tags: hit.Tags,
}
if hit.Field != nil && query.Sort.Name != "" {
fieldName, _, err := legacysearcher.ParseSortName(query.Sort.Name)
if err != nil {
return nil, err
}
result.SortMeta = hit.Field.GetNestedInt64(fieldName)
}
if hit.Resource == folderv1.RESOURCE {
result.IsFolder = true
}
finalResults[i] = result
}
return finalResults, nil
finalResults[i] = result
}
return dr.dashboardStore.FindDashboards(ctx, query)
return finalResults, nil
}
type folderRes struct {
@@ -1680,11 +1517,7 @@ func (dr *DashboardServiceImpl) SearchDashboards(ctx context.Context, query *das
}
func (dr *DashboardServiceImpl) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*dashboards.Dashboard, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return dr.listDashboardsThroughK8s(ctx, orgID)
}
return dr.dashboardStore.GetAllDashboardsByOrgId(ctx, orgID)
return dr.listDashboardsThroughK8s(ctx, orgID)
}
func getHitType(item dashboards.DashboardSearchProjection) model.HitType {
@@ -1743,51 +1576,43 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb
}
func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) {
if dr.features.IsEnabled(ctx, featuremgmt.FlagKubernetesClientDashboardsFolders) {
res, err := dr.k8sclient.Search(ctx, query.OrgID, &resourcepb.ResourceSearchRequest{
Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{
"tags": {
Field: "tags",
Limit: 100000,
},
res, err := dr.k8sclient.Search(ctx, query.OrgID, &resourcepb.ResourceSearchRequest{
Facet: map[string]*resourcepb.ResourceSearchRequest_Facet{
"tags": {
Field: "tags",
Limit: 100000,
},
Limit: 100000})
if err != nil {
return nil, err
}
facet, ok := res.Facet["tags"]
if !ok {
return []*dashboards.DashboardTagCloudItem{}, nil
}
results := make([]*dashboards.DashboardTagCloudItem, len(facet.Terms))
for i, item := range facet.Terms {
results[i] = &dashboards.DashboardTagCloudItem{
Term: item.Term,
Count: int(item.Count),
}
}
return results, nil
},
Limit: 100000})
if err != nil {
return nil, err
}
facet, ok := res.Facet["tags"]
if !ok {
return []*dashboards.DashboardTagCloudItem{}, nil
}
return dr.dashboardStore.GetDashboardTags(ctx, query)
results := make([]*dashboards.DashboardTagCloudItem, len(facet.Terms))
for i, item := range facet.Terms {
results[i] = &dashboards.DashboardTagCloudItem{
Term: item.Term,
Count: int(item.Count),
}
}
return results, nil
}
func (dr DashboardServiceImpl) CountInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) (int64, error) {
if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: orgID,
FolderUIDs: folderUIDs,
})
if err != nil {
return 0, err
}
return int64(len(dashs)), nil
dashs, err := dr.searchDashboardsThroughK8s(ctx, &dashboards.FindPersistedDashboardsQuery{
OrgId: orgID,
FolderUIDs: folderUIDs,
})
if err != nil {
return 0, err
}
return dr.dashboardStore.CountDashboardsInFolders(ctx, &dashboards.CountDashboardsInFolderRequest{FolderUIDs: folderUIDs, OrgID: orgID})
return int64(len(dashs)), nil
}
func (dr *DashboardServiceImpl) DeleteInFolders(ctx context.Context, orgID int64, folderUIDs []string, u identity.Requester) error {
File diff suppressed because it is too large Load Diff
-122
View File
@@ -62,34 +62,6 @@ func (_m *FakeDashboardStore) Count(_a0 context.Context, _a1 *quota.ScopeParamet
return r0, r1
}
// CountDashboardsInFolders provides a mock function with given fields: ctx, request
func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error) {
ret := _m.Called(ctx, request)
if len(ret) == 0 {
panic("no return value specified for CountDashboardsInFolders")
}
var r0 int64
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *CountDashboardsInFolderRequest) (int64, error)); ok {
return rf(ctx, request)
}
if rf, ok := ret.Get(0).(func(context.Context, *CountDashboardsInFolderRequest) int64); ok {
r0 = rf(ctx, request)
} else {
r0 = ret.Get(0).(int64)
}
if rf, ok := ret.Get(1).(func(context.Context, *CountDashboardsInFolderRequest) error); ok {
r1 = rf(ctx, request)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CountInOrg provides a mock function with given fields: ctx, orgID, isFolder
func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFolder bool) (int64, error) {
ret := _m.Called(ctx, orgID, isFolder)
@@ -118,23 +90,6 @@ func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFol
return r0, r1
}
// DeleteAllDashboards provides a mock function with given fields: ctx, orgID
func (_m *FakeDashboardStore) DeleteAllDashboards(ctx context.Context, orgID int64) error {
ret := _m.Called(ctx, orgID)
if len(ret) == 0 {
panic("no return value specified for DeleteAllDashboards")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, orgID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteDashboard provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error {
@@ -172,23 +127,6 @@ func (_m *FakeDashboardStore) DeleteDashboardsInFolders(ctx context.Context, req
return r0
}
// DeleteOrphanedProvisionedDashboards provides a mock function with given fields: ctx, cmd
func (_m *FakeDashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error {
ret := _m.Called(ctx, cmd)
if len(ret) == 0 {
panic("no return value specified for DeleteOrphanedProvisionedDashboards")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *DeleteOrphanedProvisionedDashboardsCommand) error); ok {
r0 = rf(ctx, cmd)
} else {
r0 = ret.Error(0)
}
return r0
}
// FindDashboards provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
@@ -310,66 +248,6 @@ func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDa
return r0, r1
}
// GetDashboardUIDByID provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboardUIDByID")
}
var r0 *DashboardRef
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) (*DashboardRef, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardRefByIDQuery) *DashboardRef); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*DashboardRef)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardRefByIDQuery) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDashboards provides a mock function with given fields: ctx, query
func (_m *FakeDashboardStore) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for GetDashboards")
}
var r0 []*Dashboard
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) ([]*Dashboard, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardsQuery) []*Dashboard); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*Dashboard)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardsQuery) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetDashboardsByLibraryPanelUID provides a mock function with given fields: ctx, libraryPanelUID, orgID
func (_m *FakeDashboardStore) GetDashboardsByLibraryPanelUID(ctx context.Context, libraryPanelUID string, orgID int64) ([]*DashboardRef, error) {
ret := _m.Called(ctx, libraryPanelUID, orgID)
@@ -4,33 +4,18 @@ import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
dashsnapdb "github.com/grafana/grafana/pkg/services/dashboardsnapshots/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
@@ -81,7 +66,6 @@ func TestIntegrationDashboardSnapshotsService(t *testing.T) {
require.Equal(t, rawDashboard, decrypted)
})
t.Run("get dashboard snapshot should return the dashboard decrypted", func(t *testing.T) {
ctx := context.Background()
@@ -99,69 +83,3 @@ func TestIntegrationDashboardSnapshotsService(t *testing.T) {
require.Equal(t, rawDashboard, decrypted)
})
}
func TestIntegrationValidateDashboardExists(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
dsStore := dashsnapdb.ProvideStore(sqlStore, cfg)
secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore))
feats := featuremgmt.WithFeatures()
dashboardStore, err := dashdb.ProvideDashboardStore(sqlStore, cfg, feats, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
dashSvc, err := dashsvc.ProvideDashboardServiceImpl(
cfg,
dashboardStore,
folderimpl.ProvideDashboardFolderStore(sqlStore),
feats,
nil,
actest.FakeAccessControl{},
actest.FakeService{},
foldertest.NewFakeService(),
nil,
client.MockTestRestConfig{},
nil,
quotatest.New(false, nil),
nil,
nil,
nil,
dualwrite.ProvideTestService(),
sort.ProvideService(),
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore(),
)
require.NoError(t, err)
s := ProvideService(dsStore, secretsService, dashSvc)
ctx := context.Background()
t.Run("returns false when dashboard does not exist", func(t *testing.T) {
err := s.ValidateDashboardExists(ctx, 1, "test")
require.Error(t, err)
require.Equal(t, dashboards.ErrDashboardNotFound, err)
})
t.Run("returns true when dashboard exists", func(t *testing.T) {
err := createDashboard(sqlStore)
require.NoError(t, err)
err = s.ValidateDashboardExists(ctx, 1, "test")
require.NoError(t, err)
})
}
func createDashboard(store db.DB) error {
return store.WithDbSession(context.Background(), func(sess *db.Session) error {
dashboard := &dashboards.Dashboard{
ID: 1,
UID: "test",
OrgID: 1,
Created: time.Now(),
Updated: time.Now(),
}
_, err := sess.Insert(dashboard)
return err
})
}
@@ -0,0 +1,75 @@
package dashboardsnapshots
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1"
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web"
)
func TestCreateDashboardSnapshot_DashboardNotFound(t *testing.T) {
mockService := &MockService{}
cfg := dashboardsnapshot.SnapshotSharingOptions{
SnapshotsEnabled: true,
ExternalEnabled: false,
}
testUser := &user.SignedInUser{
UserID: 1,
OrgID: 1,
Login: "testuser",
Name: "Test User",
Email: "test@example.com",
}
dashboard := &common.Unstructured{}
dashboardData := map[string]interface{}{
"uid": "test-dashboard-uid",
"id": 123,
}
dashboardBytes, _ := json.Marshal(dashboardData)
_ = json.Unmarshal(dashboardBytes, dashboard)
cmd := CreateDashboardSnapshotCommand{
DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{
Dashboard: dashboard,
Name: "Test Snapshot",
},
}
mockService.On("ValidateDashboardExists", mock.Anything, int64(1), "test-dashboard-uid").
Return(dashboards.ErrDashboardNotFound)
req, _ := http.NewRequest("POST", "/api/snapshots", nil)
req = req.WithContext(identity.WithRequester(req.Context(), testUser))
recorder := httptest.NewRecorder()
ctx := &contextmodel.ReqContext{
Context: &web.Context{
Req: req,
Resp: web.NewResponseWriter("POST", recorder),
},
SignedInUser: testUser,
Logger: log.NewNopLogger(),
}
CreateDashboardSnapshot(ctx, cfg, cmd, mockService)
mockService.AssertExpectations(t)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
var response map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, "Dashboard not found", response["message"])
}
@@ -89,20 +89,11 @@ func (s *Service) Get(ctx context.Context, query *dashver.GetDashboardVersionQue
query.DashboardID = id
}
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
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)
version, err := s.getHistoryThroughK8s(ctx, query.OrgID, query.DashboardUID, query.Version)
if err != nil {
return nil, err
}
version.Data.Set("id", version.DashboardID)
return version.ToDTO(query.DashboardUID), nil
return version, nil
}
func (s *Service) DeleteExpired(ctx context.Context, cmd *dashver.DeleteExpiredVersionsCommand) error {
@@ -160,31 +151,17 @@ func (s *Service) List(ctx context.Context, query *dashver.ListDashboardVersions
query.Limit = 1000
}
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
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)
versions, err := s.listHistoryThroughK8s(
ctx,
query.OrgID,
query.DashboardUID,
int64(query.Limit),
query.ContinueToken,
)
if err != nil {
return nil, err
}
dtos := make([]*dashver.DashboardVersionDTO, len(dvs))
for i, v := range dvs {
dtos[i] = v.ToDTO(query.DashboardUID)
}
return &dashver.DashboardVersionResponse{
Versions: dtos,
}, nil
return versions, nil
}
// getDashUIDMaybeEmpty is a helper function which takes a dashboardID and
@@ -15,7 +15,6 @@ import (
"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"
@@ -25,29 +24,12 @@ import (
)
func TestDashboardVersionService(t *testing.T) {
dashboardVersionStore := newDashboardVersionStoreFake()
dashboardService := dashboards.NewFakeDashboardService(t)
dashboardVersionService := Service{store: dashboardVersionStore, dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
t.Run("Get dashboard version", func(t *testing.T) {
dashboard := &dashver.DashboardVersion{
ID: 11,
Data: &simplejson.Json{},
}
dashboardVersionStore.ExpectedDashboardVersion = dashboard
dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil)
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(&dashboards.Dashboard{ID: 42}, nil)
dashboardVersion, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{})
require.NoError(t, err)
require.Equal(t, dashboard.ToDTO("uid"), dashboardVersion)
})
t.Run("Get dashboard versions through k8s", func(t *testing.T) {
t.Run("Get dashboard versions", 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.FlagKubernetesClientDashboardsFolders)
dashboardVersionService.features = featuremgmt.WithFeatures()
dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil)
creationTimestamp := time.Now().Add(time.Hour * -24).UTC()
@@ -133,7 +115,7 @@ func TestDashboardVersionService(t *testing.T) {
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
mockCli := new(client.MockK8sHandler)
dashboardVersionService.k8sclient = mockCli
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders)
dashboardVersionService.features = featuremgmt.WithFeatures()
dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).Return(&dashboards.DashboardRef{UID: "uid"}, nil)
mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(nil, apierrors.NewNotFound(schema.GroupResource{Group: "dashboards.dashboard.grafana.app", Resource: "dashboard"}, "uid"))
@@ -176,93 +158,12 @@ func TestDeleteExpiredVersions(t *testing.T) {
}
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, features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{
{ID: 1, DashboardID: 42},
}
dashboardService.On("GetDashboardUIDByID", mock.Anything,
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
query := dashver.ListDashboardVersionsQuery{DashboardID: 42}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res.Versions))
// validate that the UID was populated
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(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{
{ID: 1, DashboardID: 42},
}
dashboardService.On("GetDashboardUIDByID", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
Return(nil, dashboards.ErrDashboardNotFound).Once()
query := dashver.ListDashboardVersionsQuery{DashboardID: 42}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
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.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(), 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)
query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
require.Equal(t, 1, len(res.Versions))
// validate that the dashboardID was populated from the GetDashboard method call.
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(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedListVersions = []*dashver.DashboardVersion{{DashboardID: 42, ID: 1}}
dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).
Return(nil, dashboards.ErrDashboardNotFound)
query := dashver.ListDashboardVersionsQuery{DashboardUID: "uid"}
res, err := dashboardVersionService.List(context.Background(), &query)
require.Nil(t, err)
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.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(), features: featuremgmt.WithFeatures()}
dashboardVersionStore.ExpectedError = dashver.ErrDashboardVersionNotFound
query := dashver.ListDashboardVersionsQuery{DashboardID: 42, DashboardUID: "42"}
res, err := dashboardVersionService.List(context.Background(), &query)
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.FlagKubernetesClientDashboardsFolders)
dashboardVersionService.features = featuremgmt.WithFeatures()
dashboardService.On("GetDashboardUIDByID", mock.Anything,
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
@@ -301,7 +202,7 @@ func TestListDashboardVersions(t *testing.T) {
dashboardVersionService := Service{dashSvc: dashboardService, features: featuremgmt.WithFeatures()}
mockCli := new(client.MockK8sHandler)
dashboardVersionService.k8sclient = mockCli
dashboardVersionService.features = featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders)
dashboardVersionService.features = featuremgmt.WithFeatures()
dashboardService.On("GetDashboardUIDByID", mock.Anything,
mock.AnythingOfType("*dashboards.GetDashboardRefByIDQuery")).
Return(&dashboards.DashboardRef{UID: "uid"}, nil)
-7
View File
@@ -483,13 +483,6 @@ var (
Owner: grafanaAppPlatformSquad,
FrontendOnly: true,
},
{
Name: "kubernetesClientDashboardsFolders",
Description: "Route the folder and dashboard service requests to k8s",
Stage: FeatureStageGeneralAvailability,
Owner: grafanaAppPlatformSquad,
Expression: "true", // enabled by default
},
{
Name: "dashboardDisableSchemaValidationV1",
Description: "Disable schema validation for dashboards/v1",
@@ -440,7 +440,6 @@ alertingConversionAPI,2025-02-12T07:13:21Z,2025-04-05T08:27:02Z,9593e51da7c05ed5
alertingJiraIntegration,2025-02-14T12:22:04Z,,af8cab92109ecfbb54625ff25a3b90a4f92dc46a,Sonia Aguilar
alertingRuleVersionHistoryRestore,2025-02-17T12:25:32Z,,2014d27defe5668ba07913cec6f2186c90eaaab2,Sonia Aguilar
newShareReportDrawer,2025-02-17T19:05:46Z,,9df6412e92559f2329c2baac0025f56fc57c5bf9,Ezequiel Victorero
kubernetesClientDashboardsFolders,2025-02-18T23:11:26Z,,3e6f40c87386984be60e391b26a7559e3469dac3,Stephanie Hingtgen
managedDualWriter,2025-02-19T14:50:39Z,,5a40c84568485da55ccb42998c6d291d310abd56,Ryan McKinley
rendererDisableAppPluginsPreload,2025-02-24T14:43:06Z,,608d974585c696253ac629f3c7bfc3a0043cbd49,Agnès Toulet
assetSriChecks,2025-03-04T10:56:35Z,,bbfeb8d220cc67c329aa2b5d6ed693ae1cb54325,Jack Westbrook
1 #name created deleted hash author
440 alertingJiraIntegration 2025-02-14T12:22:04Z af8cab92109ecfbb54625ff25a3b90a4f92dc46a Sonia Aguilar
441 alertingRuleVersionHistoryRestore 2025-02-17T12:25:32Z 2014d27defe5668ba07913cec6f2186c90eaaab2 Sonia Aguilar
442 newShareReportDrawer 2025-02-17T19:05:46Z 9df6412e92559f2329c2baac0025f56fc57c5bf9 Ezequiel Victorero
kubernetesClientDashboardsFolders 2025-02-18T23:11:26Z 3e6f40c87386984be60e391b26a7559e3469dac3 Stephanie Hingtgen
443 managedDualWriter 2025-02-19T14:50:39Z 5a40c84568485da55ccb42998c6d291d310abd56 Ryan McKinley
444 rendererDisableAppPluginsPreload 2025-02-24T14:43:06Z 608d974585c696253ac629f3c7bfc3a0043cbd49 Agnès Toulet
445 assetSriChecks 2025-03-04T10:56:35Z bbfeb8d220cc67c329aa2b5d6ed693ae1cb54325 Jack Westbrook
-1
View File
@@ -62,7 +62,6 @@ kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,
kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesLibraryPanelConnections,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true
kubernetesClientDashboardsFolders,GA,@grafana/grafana-app-platform-squad,false,false,false
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
62 kubernetesLibraryPanels experimental @grafana/grafana-app-platform-squad false true false
63 kubernetesLibraryPanelConnections experimental @grafana/grafana-app-platform-squad false true false
64 kubernetesDashboards experimental @grafana/grafana-app-platform-squad false false true
kubernetesClientDashboardsFolders GA @grafana/grafana-app-platform-squad false false false
65 dashboardDisableSchemaValidationV1 experimental @grafana/grafana-app-platform-squad false false false
66 dashboardDisableSchemaValidationV2 experimental @grafana/grafana-app-platform-squad false false false
67 dashboardSchemaValidationLogging experimental @grafana/grafana-app-platform-squad false false false
-4
View File
@@ -259,10 +259,6 @@ const (
// Use the kubernetes API in the frontend for dashboards
FlagKubernetesDashboards = "kubernetesDashboards"
// FlagKubernetesClientDashboardsFolders
// Route the folder and dashboard service requests to k8s
FlagKubernetesClientDashboardsFolders = "kubernetesClientDashboardsFolders"
// FlagDashboardDisableSchemaValidationV1
// Disable schema validation for dashboards/v1
FlagDashboardDisableSchemaValidationV1 = "dashboardDisableSchemaValidationV1"
-13
View File
@@ -1729,19 +1729,6 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "kubernetesClientDashboardsFolders",
"resourceVersion": "1753448760331",
"creationTimestamp": "2025-02-18T23:11:26Z"
},
"spec": {
"description": "Route the folder and dashboard service requests to k8s",
"stage": "GA",
"codeowner": "@grafana/grafana-app-platform-squad",
"expression": "true"
}
},
{
"metadata": {
"name": "kubernetesDashboards",
+40 -79
View File
@@ -112,39 +112,35 @@ func ProvideService(
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv))
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
k8sHandler := client.NewK8sHandler(
dual,
request.GetNamespaceMapper(cfg),
folderv1.FolderResourceInfo.GroupVersionResource(),
restConfig.GetRestConfig,
dashboardStore,
userService,
resourceClient,
sorter,
features,
)
k8sHandler := client.NewK8sHandler(
dual,
request.GetNamespaceMapper(cfg),
folderv1.FolderResourceInfo.GroupVersionResource(),
restConfig.GetRestConfig,
dashboardStore,
userService,
resourceClient,
sorter,
features,
)
unifiedStore := ProvideUnifiedStore(k8sHandler, userService, tracer)
unifiedStore := ProvideUnifiedStore(k8sHandler, userService, tracer)
srv.unifiedStore = unifiedStore
srv.k8sclient = k8sHandler
}
srv.unifiedStore = unifiedStore
srv.k8sclient = k8sHandler
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
dashHandler := client.NewK8sHandler(
dual,
request.GetNamespaceMapper(cfg),
dashboardv1.DashboardResourceInfo.GroupVersionResource(),
restConfig.GetRestConfig,
dashboardStore,
userService,
resourceClient,
sorter,
features,
)
srv.dashboardK8sClient = dashHandler
}
dashHandler := client.NewK8sHandler(
dual,
request.GetNamespaceMapper(cfg),
dashboardv1.DashboardResourceInfo.GroupVersionResource(),
restConfig.GetRestConfig,
dashboardStore,
userService,
resourceClient,
sorter,
features,
)
srv.dashboardK8sClient = dashHandler
return srv
}
@@ -201,32 +197,22 @@ func (s *Service) DBMigration(db db.DB) {
func (s *Service) CountFoldersInOrg(ctx context.Context, orgID int64) (int64, error) {
ctx, span := s.tracer.Start(ctx, "folder.CountFoldersInOrg")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.unifiedStore.CountInOrg(ctx, orgID)
}
return s.store.CountInOrg(ctx, orgID)
return s.unifiedStore.CountInOrg(ctx, orgID)
}
func (s *Service) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) {
ctx, span := s.tracer.Start(ctx, "folder.SearchFolders")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
// TODO:
// - implement filtering by alerting folders and k6 folders (see the dashboards store `FindDashboards` method for reference)
// - implement fallback on search client in unistore to go to legacy store (will need to read from dashboard store)
return s.searchFoldersFromApiServer(ctx, q)
}
return nil, fmt.Errorf("cannot be called on the legacy folder service")
// TODO:
// - implement filtering by alerting folders and k6 folders (see the dashboards store `FindDashboards` method for reference)
// - implement fallback on search client in unistore to go to legacy store (will need to read from dashboard store)
return s.searchFoldersFromApiServer(ctx, q)
}
func (s *Service) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetFolders")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.getFoldersFromApiServer(ctx, q)
}
return s.GetFoldersLegacy(ctx, q)
return s.getFoldersFromApiServer(ctx, q)
}
func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
@@ -286,10 +272,7 @@ func (s *Service) GetFoldersLegacy(ctx context.Context, q folder.GetFoldersQuery
func (s *Service) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Get")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.getFromApiServer(ctx, q)
}
return s.GetLegacy(ctx, q)
return s.getFromApiServer(ctx, q)
}
func (s *Service) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
@@ -377,10 +360,7 @@ func (s *Service) setFullpath(ctx context.Context, f *folder.Folder, forceLegacy
func (s *Service) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetChildren")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.getChildrenFromApiServer(ctx, q)
}
return s.GetChildrenLegacy(ctx, q)
return s.getChildrenFromApiServer(ctx, q)
}
func (s *Service) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
@@ -660,10 +640,7 @@ func (s *Service) deduplicateAvailableFolders(ctx context.Context, folders []*fo
func (s *Service) GetParents(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetParents")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.getParentsFromApiServer(ctx, q)
}
return s.GetParentsLegacy(ctx, q)
return s.getParentsFromApiServer(ctx, q)
}
func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery) ([]*folder.Folder, error) {
@@ -679,10 +656,7 @@ func (s *Service) GetParentsLegacy(ctx context.Context, q folder.GetParentsQuery
}
func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.createOnApiServer(ctx, cmd)
}
return s.CreateLegacy(ctx, cmd)
return s.createOnApiServer(ctx, cmd)
}
func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
@@ -800,10 +774,7 @@ func (s *Service) CreateLegacy(ctx context.Context, cmd *folder.CreateFolderComm
func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Update")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.updateOnApiServer(ctx, cmd)
}
return s.UpdateLegacy(ctx, cmd)
return s.updateOnApiServer(ctx, cmd)
}
func (s *Service) UpdateLegacy(ctx context.Context, cmd *folder.UpdateFolderCommand) (*folder.Folder, error) {
@@ -943,10 +914,7 @@ func prepareForUpdate(dashFolder *dashboards.Dashboard, orgId int64, userId int6
func (s *Service) Delete(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
ctx, span := s.tracer.Start(ctx, "folder.Delete")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.deleteFromApiServer(ctx, cmd)
}
return s.DeleteLegacy(ctx, cmd)
return s.deleteFromApiServer(ctx, cmd)
}
func (s *Service) DeleteLegacy(ctx context.Context, cmd *folder.DeleteFolderCommand) error {
@@ -1067,10 +1035,7 @@ func (s *Service) legacyDelete(ctx context.Context, cmd *folder.DeleteFolderComm
func (s *Service) Move(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
ctx, span := s.tracer.Start(ctx, "folder.Move")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.moveOnApiServer(ctx, cmd)
}
return s.MoveLegacy(ctx, cmd)
return s.moveOnApiServer(ctx, cmd)
}
func (s *Service) MoveLegacy(ctx context.Context, cmd *folder.MoveFolderCommand) (*folder.Folder, error) {
@@ -1296,11 +1261,7 @@ func (s *Service) nestedFolderDelete(ctx context.Context, cmd *folder.DeleteFold
func (s *Service) GetDescendantCounts(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
ctx, span := s.tracer.Start(ctx, "folder.GetDescendantCounts")
defer span.End()
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
return s.getDescendantCountsFromApiServer(ctx, q)
}
return s.GetDescendantCountsLegacy(ctx, q)
return s.getDescendantCountsFromApiServer(ctx, q)
}
func (s *Service) GetDescendantCountsLegacy(ctx context.Context, q *folder.GetDescendantCountsQuery) (folder.DescendantCounts, error) {
@@ -193,9 +193,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) {
ExpectedUser: &user.User{},
}
featuresArr := []any{
featuremgmt.FlagKubernetesClientDashboardsFolders}
features := featuremgmt.WithFeatures(featuresArr...)
features := featuremgmt.WithFeatures()
tracer := noop.NewTracerProvider().Tracer("TestIntegrationFolderServiceViaUnifiedStorage")
dashboardStore := dashboards.NewFakeDashboardStore(t)
@@ -521,7 +519,7 @@ func TestSearchFoldersFromApiServer(t *testing.T) {
tracer := noop.NewTracerProvider().Tracer("TestSearchFoldersFromApiServer")
service := Service{
k8sclient: fakeK8sClient,
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders),
features: featuremgmt.WithFeatures(),
unifiedStore: folderStore,
tracer: tracer,
accessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
@@ -764,7 +762,7 @@ func TestGetFoldersFromApiServer(t *testing.T) {
tracer := noop.NewTracerProvider().Tracer("TestGetFoldersFromApiServer")
service := Service{
k8sclient: fakeK8sClient,
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders),
features: featuremgmt.WithFeatures(),
unifiedStore: folderStore,
accessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
tracer: tracer,
@@ -866,7 +864,7 @@ func TestIntegrationDeleteFoldersFromApiServer(t *testing.T) {
publicDashboardService: publicDashboardFakeService,
accessControl: actest.FakeAccessControl{ExpectedEvaluate: true},
registry: make(map[string]folder.RegistryService),
features: featuremgmt.WithFeatures(featuremgmt.FlagKubernetesClientDashboardsFolders),
features: featuremgmt.WithFeatures(),
tracer: tracer,
}
user := &user.SignedInUser{OrgID: 1}
+57 -3
View File
@@ -15,16 +15,49 @@ type FakeService struct {
ExpectedError error
ExpectedDescendantCounts map[string]int64
LastQuery folder.GetFoldersQuery
foldersByUID map[string]*folder.Folder
}
func NewFakeService() *FakeService {
return &FakeService{}
return &FakeService{
foldersByUID: make(map[string]*folder.Folder),
}
}
func (s *FakeService) AddFolder(f *folder.Folder) {
if s.foldersByUID == nil {
s.foldersByUID = make(map[string]*folder.Folder)
}
s.foldersByUID[f.UID] = f
s.ExpectedFolders = append(s.ExpectedFolders, f)
}
func (s *FakeService) SetFolders(folders map[string]*folder.Folder) {
s.foldersByUID = folders
s.ExpectedFolders = make([]*folder.Folder, 0, len(folders))
for _, f := range folders {
s.ExpectedFolders = append(s.ExpectedFolders, f)
}
}
var _ folder.Service = (*FakeService)(nil)
func (s *FakeService) GetChildren(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
return s.ExpectedFoldersRef, s.ExpectedError
if s.ExpectedError != nil {
return nil, s.ExpectedError
}
if s.ExpectedFoldersRef != nil {
return s.ExpectedFoldersRef, nil
}
var result []*folder.FolderReference
for _, f := range s.ExpectedFolders {
if f.OrgID == q.OrgID && f.ParentUID == q.UID {
result = append(result, f.ToFolderReference())
}
}
return result, nil
}
func (s *FakeService) GetChildrenLegacy(ctx context.Context, q *folder.GetChildrenQuery) ([]*folder.FolderReference, error) {
return s.ExpectedFoldersRef, s.ExpectedError
@@ -45,6 +78,11 @@ func (s *FakeService) CreateLegacy(ctx context.Context, cmd *folder.CreateFolder
}
func (s *FakeService) Get(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
if q.UID != nil && s.foldersByUID != nil {
if f, exists := s.foldersByUID[*q.UID]; exists {
return f, nil
}
}
return s.ExpectedFolder, s.ExpectedError
}
func (s *FakeService) GetLegacy(ctx context.Context, q *folder.GetFolderQuery) (*folder.Folder, error) {
@@ -84,7 +122,23 @@ func (s *FakeService) GetDescendantCountsLegacy(ctx context.Context, q *folder.G
}
func (s *FakeService) GetFolders(ctx context.Context, q folder.GetFoldersQuery) ([]*folder.Folder, error) {
return s.ExpectedFolders, s.ExpectedError
if s.foldersByUID != nil && len(q.UIDs) > 0 {
var result []*folder.Folder
for _, uid := range q.UIDs {
if f, exists := s.foldersByUID[uid]; exists {
result = append(result, f)
}
}
return result, nil
}
folders := make([]*folder.Folder, 0, len(s.ExpectedFolders))
for _, f := range s.ExpectedFolders {
if f.OrgID == q.OrgID {
folders = append(folders, f)
}
}
return folders, s.ExpectedError
}
func (s *FakeService) SearchFolders(ctx context.Context, q folder.SearchFoldersQuery) (model.HitList, error) {
+8 -27
View File
@@ -893,39 +893,20 @@ func getFoldersWithMatchingTitles(c context.Context, l *LibraryElementService, s
return nil, nil
}
if l.features.IsEnabled(c, featuremgmt.FlagKubernetesClientDashboardsFolders) {
searchQuery := folder.SearchFoldersQuery{
OrgID: signedInUser.GetOrgID(),
Title: query.SearchString,
SignedInUser: signedInUser,
}
folderHits, err := l.folderService.SearchFolders(c, searchQuery)
if err != nil {
return nil, err
}
foldersWithMatchingTitles := make([]string, 0, len(folderHits))
for _, hit := range folderHits {
foldersWithMatchingTitles = append(foldersWithMatchingTitles, hit.UID)
}
return foldersWithMatchingTitles, nil
searchQuery := folder.SearchFoldersQuery{
OrgID: signedInUser.GetOrgID(),
Title: query.SearchString,
SignedInUser: signedInUser,
}
// Fallback to GetFolders
fs, err := l.folderService.GetFolders(c, folder.GetFoldersQuery{
OrgID: signedInUser.GetOrgID(),
SignedInUser: signedInUser,
})
folderHits, err := l.folderService.SearchFolders(c, searchQuery)
if err != nil {
return nil, err
}
foldersWithMatchingTitles := make([]string, 0, len(fs))
for _, f := range fs {
if strings.Contains(strings.ToLower(f.Title), strings.ToLower(query.SearchString)) {
foldersWithMatchingTitles = append(foldersWithMatchingTitles, f.UID)
}
foldersWithMatchingTitles := make([]string, 0, len(folderHits))
for _, hit := range folderHits {
foldersWithMatchingTitles = append(foldersWithMatchingTitles, hit.UID)
}
return foldersWithMatchingTitles, nil
}
@@ -46,8 +46,8 @@ func TestIntegration_CreateLibraryElement(t *testing.T) {
},
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: "uid_for_ScenarioFolder",
FolderName: sc.folder.Title,
FolderUID: sc.folder.UID,
ConnectedDashboards: 0,
Created: sc.initialResult.Result.Meta.Created,
Updated: sc.initialResult.Result.Meta.Updated,
@@ -97,8 +97,8 @@ func TestIntegration_CreateLibraryElement(t *testing.T) {
},
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: "uid_for_ScenarioFolder",
FolderName: sc.folder.Title,
FolderUID: sc.folder.UID,
ConnectedDashboards: 0,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
@@ -176,8 +176,8 @@ func TestIntegration_CreateLibraryElement(t *testing.T) {
},
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderUID: "uid_for_ScenarioFolder",
FolderName: sc.folder.Title,
FolderUID: sc.folder.UID,
ConnectedDashboards: 0,
Created: result.Result.Meta.Created,
Updated: result.Result.Meta.Updated,
@@ -4,9 +4,9 @@ import (
"encoding/json"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
@@ -25,6 +25,7 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to delete a library panel that exists, it should succeed and return correct ID",
func(t *testing.T, sc scenarioContext) {
sc.dashboardSvc.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil)
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.deleteHandler(sc.reqContext)
require.Equal(t, 200, resp.Status())
@@ -47,39 +48,14 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected, it should fail",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]any{
"panels": []any{
map[string]any{
"id": int64(1),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]any{
"id": int64(2),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]any{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
sc.dashboardSvc.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{
{
ID: 1,
UID: "test",
},
}
dash := dashboards.Dashboard{
Title: "Testing deleteHandler ",
Data: simplejson.NewFromAny(dashJSON),
}
}, nil)
// nolint:staticcheck
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
require.NoError(t, err)
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
@@ -89,6 +65,7 @@ func TestIntegration_DeleteLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to delete a library panel that is connected to a non-existent dashboard, it should succeed",
func(t *testing.T, sc scenarioContext) {
sc.dashboardSvc.On("FindDashboards", mock.Anything, mock.Anything).Return([]dashboards.DashboardSearchProjection{}, nil)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 9999999)
require.NoError(t, err)
@@ -8,8 +8,10 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
searchmodel "github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/services/search/sort"
)
@@ -406,7 +408,14 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get all library panels and two exist and folderFilterUIDs is set to existing folders, it should succeed and the result should be correct",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
newFolder := &folder.Folder{
ID: 2,
OrgID: 1,
UID: "uid_for_NewFolder",
Title: "NewFolder",
}
sc.folderSvc.ExpectedFolder = newFolder
sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder}
// nolint:staticcheck
command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel2")
sc.reqContext.Req.Body = mockRequestBody(command)
@@ -448,7 +457,7 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) {
},
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "NewFolder",
FolderName: newFolder.Title,
FolderUID: newFolder.UID,
ConnectedDashboards: 0,
Created: result.Result.Elements[0].Meta.Created,
@@ -1171,6 +1180,14 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) {
// Folder name search integration tests
scenarioWithPanel(t, "When searching by folder name, it should return panels in that folder",
func(t *testing.T, sc scenarioContext) {
sc.folderSvc.ExpectedHitList = searchmodel.HitList{
{
UID: sc.folder.UID,
Title: sc.folder.Title,
Type: searchmodel.DashHitFolder,
},
}
// Create a panel in the existing folder
// nolint:staticcheck
command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Panel in ScenarioFolder")
@@ -1275,6 +1292,14 @@ func TestIntegration_GetAllLibraryElements(t *testing.T) {
scenarioWithPanel(t, "When searching by partial folder name, it should return panels in matching folders",
func(t *testing.T, sc scenarioContext) {
sc.folderSvc.ExpectedHitList = searchmodel.HitList{
{
UID: sc.folder.UID,
Title: sc.folder.Title,
Type: searchmodel.DashHitFolder,
},
}
// Create a panel in the existing folder
// nolint:staticcheck
command := getCreatePanelCommand(sc.folder.ID, sc.folder.UID, "Test Panel")
@@ -7,7 +7,6 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -57,7 +56,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
},
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderName: sc.folder.Title,
FolderUID: sc.folder.UID,
ConnectedDashboards: 0,
Created: res.Result.Meta.Created,
@@ -120,6 +119,8 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
SignedInUser: sc.reqContext.SignedInUser,
})
require.NoError(t, err)
sc.folderSvc.ExpectedFolder = nil
sc.folderSvc.ExpectedError = folder.ErrFolderNotFound
err = sc.sqlStore.WithDbSession(sc.reqContext.Req.Context(), func(session *db.Session) error {
elem, err := sc.service.GetLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, session, result.UID)
require.NoError(t, err)
@@ -131,39 +132,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to get a connected library panel, it should succeed and return correct connected dashboards",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]any{
"panels": []any{
map[string]any{
"id": int64(1),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]any{
"id": int64(2),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]any{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := dashboards.Dashboard{
Title: "Testing getHandler",
Data: simplejson.NewFromAny(dashJSON),
}
// nolint:staticcheck
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
require.NoError(t, err)
expected := func(res libraryElementResult) libraryElementResult {
@@ -187,7 +156,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
},
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "ScenarioFolder",
FolderName: sc.folder.Title,
FolderUID: sc.folder.UID,
ConnectedDashboards: 1,
Created: res.Result.Meta.Created,
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/util"
@@ -28,7 +29,14 @@ func TestIntegration_PatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel that exists, it should succeed",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
newFolder := &folder.Folder{
ID: 2,
OrgID: 1,
UID: "uid_for_NewFolder",
Title: "NewFolder",
}
sc.folderSvc.ExpectedFolder = newFolder
sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder}
cmd := model.PatchLibraryElementCommand{
FolderID: newFolder.ID, // nolint:staticcheck
FolderUID: &newFolder.UID,
@@ -95,7 +103,14 @@ func TestIntegration_PatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with folder only, it should change folder successfully and return correct result",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
newFolder := &folder.Folder{
ID: 2,
OrgID: 1,
UID: "uid_for_NewFolder",
Title: "NewFolder",
}
sc.folderSvc.ExpectedFolder = newFolder
sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder}
cmd := model.PatchLibraryElementCommand{
FolderID: newFolder.ID, // nolint:staticcheck
FolderUID: &newFolder.UID,
@@ -340,26 +355,35 @@ func TestIntegration_PatchLibraryElement(t *testing.T) {
scenarioWithPanel(t, "When an admin tries to patch a library panel with a folder where a library panel with the same name already exists, it should fail",
func(t *testing.T, sc scenarioContext) {
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
newFolder := &folder.Folder{
ID: 2,
OrgID: 1,
UID: "uid_for_NewFolder",
Title: "NewFolder",
}
sc.folderSvc.ExpectedFolder = newFolder
sc.folderSvc.ExpectedFolders = []*folder.Folder{newFolder}
// nolint:staticcheck
command := getCreatePanelCommand(newFolder.ID, newFolder.UID, "Text - Library Panel")
sc.ctx.Req.Body = mockRequestBody(command)
resp := sc.service.createHandler(sc.reqContext)
var result = validateAndUnMarshalResponse(t, resp)
sc.service.createHandler(sc.reqContext)
cmd := model.PatchLibraryElementCommand{
FolderID: 1, // nolint:staticcheck
FolderUID: &sc.folder.UID,
FolderID: newFolder.ID, // nolint:staticcheck
FolderUID: &newFolder.UID,
Version: 1,
Kind: int64(model.PanelElement),
}
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": result.Result.UID})
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
sc.ctx.Req.Body = mockRequestBody(cmd)
resp = sc.service.patchHandler(sc.reqContext)
resp := sc.service.patchHandler(sc.reqContext)
require.Equal(t, 400, resp.Status())
})
scenarioWithPanel(t, "When an admin tries to patch a library panel in another org, it should fail",
func(t *testing.T, sc scenarioContext) {
sc.folderSvc.ExpectedFolder = nil
sc.folderSvc.ExpectedError = folder.ErrFolderNotFound
cmd := model.PatchLibraryElementCommand{
FolderID: sc.folder.ID, // nolint:staticcheck
FolderUID: &sc.folder.UID,
@@ -16,26 +16,17 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/client"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
@@ -43,13 +34,9 @@ import (
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/publicdashboards"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/web"
)
@@ -67,39 +54,7 @@ func TestIntegration_DeleteLibraryPanelsInFolder(t *testing.T) {
}
scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, it should fail",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]any{
"panels": []any{
map[string]any{
"id": int64(1),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]any{
"id": int64(2),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]any{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := dashboards.Dashboard{
Title: "Testing DeleteLibraryElementsInFolder",
Data: simplejson.NewFromAny(dashJSON),
}
// nolint:staticcheck
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
require.NoError(t, err)
err = sc.service.DeleteLibraryElementsInFolder(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, sc.folder.UID)
@@ -148,39 +103,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
}
scenarioWithPanel(t, "When an admin tries to get connections of library panel, it should succeed and return correct result",
func(t *testing.T, sc scenarioContext) {
dashJSON := map[string]any{
"panels": []any{
map[string]any{
"id": int64(1),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 0,
"y": 0,
},
},
map[string]any{
"id": int64(2),
"gridPos": map[string]any{
"h": 6,
"w": 6,
"x": 6,
"y": 0,
},
"libraryPanel": map[string]any{
"uid": sc.initialResult.Result.UID,
"name": sc.initialResult.Result.Name,
},
},
},
}
dash := dashboards.Dashboard{
Title: "Testing GetLibraryPanelConnections",
Data: simplejson.NewFromAny(dashJSON),
}
// nolint:staticcheck
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID)
err := sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
require.NoError(t, err)
// add a connection where the dashboard doesn't exist. Shouldn't be returned in the list
@@ -191,12 +114,11 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
return model.LibraryElementConnectionsResponse{
Result: []model.LibraryElementConnectionDTO{
{
ID: sc.initialResult.Result.ID,
Kind: sc.initialResult.Result.Kind,
ElementID: 1,
ConnectionID: dashInDB.ID,
ConnectionUID: dashInDB.UID,
Created: res.Result[0].Created,
ID: sc.initialResult.Result.ID,
Kind: sc.initialResult.Result.Kind,
ElementID: 1,
ConnectionID: 1,
Created: res.Result[0].Created,
CreatedBy: librarypanel.LibraryElementDTOMetaUser{
Id: 1,
Name: userInDbName,
@@ -207,6 +129,13 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
}
}
sc.dashboardSvc.On("GetDashboardsByLibraryPanelUID", mock.Anything, mock.Anything, mock.Anything).Return([]*dashboards.DashboardRef{
{
ID: 1,
UID: "",
},
}, nil)
sc.ctx.Req = web.SetURLParams(sc.ctx.Req, map[string]string{":uid": sc.initialResult.Result.UID})
resp := sc.service.getConnectionsHandler(sc.reqContext)
var result = validateAndUnMarshalConnectionResponse(t, resp)
@@ -239,13 +168,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
})
require.NoError(t, err)
dash := dashboards.Dashboard{
Title: "Testing create element",
Data: simplejson.NewFromAny(map[string]any{}),
}
// nolint:staticcheck
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash, sc.folder.ID, sc.folder.UID)
err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, dashInDB.ID)
err = sc.service.ConnectElementsToDashboard(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, []string{sc.initialResult.Result.UID}, 1)
require.NoError(t, err)
})
}
@@ -319,77 +242,26 @@ type scenarioContext struct {
initialResult libraryElementResult
sqlStore db.DB
log log.Logger
folderSvc folder.Service
folderSvc *foldertest.FakeService
dashboardSvc *dashboards.FakeDashboardService
}
func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash *dashboards.Dashboard, folderID int64, folderUID string) *dashboards.Dashboard {
// nolint:staticcheck
dash.FolderID = folderID
dash.FolderUID = folderUID
dashItem := &dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: "",
OrgID: user.OrgID,
User: &user,
Overwrite: false,
}
features := featuremgmt.WithFeatures()
cfg := setting.NewCfg()
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
ac := actest.FakeAccessControl{ExpectedEvaluate: true}
folderPermissions := acmock.NewMockedPermissionsService()
folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderSvc := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
_, err = folderSvc.Create(context.Background(), &folder.CreateFolderCommand{UID: folderUID, SignedInUser: &user, Title: folderUID + "-title"})
require.NoError(t, err)
service, err := dashboardservice.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore,
features, folderPermissions, ac,
actest.FakeService{},
folderSvc,
nil,
client.MockTestRestConfig{},
nil,
quotaService,
nil,
nil,
nil,
dualwrite.ProvideTestService(),
sort.ProvideService(),
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore(),
)
require.NoError(t, err)
service.RegisterDashboardPermissions(dashboardPermissions)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
require.NoError(t, err)
return dashboard
}
func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc folder.Service) *folder.Folder {
func createFolder(t *testing.T, sc scenarioContext, title string, folderSvc *foldertest.FakeService) *folder.Folder {
t.Helper()
ctx := identity.WithRequester(context.Background(), &sc.user)
folder, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{
f, err := folderSvc.Create(ctx, &folder.CreateFolderCommand{
OrgID: sc.user.OrgID, Title: title, UID: "uid_for_" + title, SignedInUser: &sc.user,
})
require.NoError(t, err)
folderSvc.ExpectedFolder = f
folderSvc.ExpectedFolders = append(folderSvc.ExpectedFolders, f)
// Set user permissions on the newly created folder so that they can interact with library elements stored in it
sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID))
sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID))
sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate], dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.UID))
sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersWrite], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionFoldersRead], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate] = append(sc.reqContext.Permissions[sc.user.OrgID][dashboards.ActionDashboardsCreate], dashboards.ScopeFoldersProvider.GetResourceScopeUID(f.UID))
return folder
return f
}
func validateAndUnMarshalResponse(t *testing.T, resp response.Response) libraryElementResult {
@@ -465,32 +337,28 @@ func setupTestScenario(t *testing.T) scenarioContext {
sqlStore, cfg := db.InitTestDBWithCfg(t)
t.Cleanup(db.CleanupTestDB)
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
ac := acimpl.ProvideAccessControl(features)
folderPermissions := acmock.NewMockedPermissionsService()
folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
dashboardPermissions := acmock.NewMockedPermissionsService()
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
publicDash := &publicdashboards.FakePublicDashboardServiceWrapper{}
publicDash.On("DeleteByDashboardUIDs", mock.Anything, mock.Anything, mock.Anything).Return(nil)
folderSvc := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), publicDash, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
folderSvc := foldertest.NewFakeService()
f := &folder.Folder{
ID: 1,
OrgID: 1,
UID: "uid_for_ScenarioFolder",
Title: "ScenarioFolder",
}
folderSvc.ExpectedFolder = f
folderSvc.ExpectedFolders = []*folder.Folder{f}
dashService := dashboards.NewFakeDashboardService(t)
alertStore, err := ngstore.ProvideDBStore(cfg, features, sqlStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
require.NoError(t, err)
err = folderSvc.RegisterService(alertStore)
require.NoError(t, err)
dashService, dashSvcErr := dashboardservice.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore,
features, folderPermissions, ac, actest.FakeService{}, folderSvc,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore(),
)
require.NoError(t, dashSvcErr)
dashService.RegisterDashboardPermissions(dashboardPermissions)
service := LibraryElementService{
Cfg: cfg,
features: featuremgmt.WithFeatures(),
@@ -530,7 +398,8 @@ func setupTestScenario(t *testing.T) scenarioContext {
Context: &webCtx,
SignedInUser: &usr,
},
folderSvc: folderSvc,
folderSvc: folderSvc,
dashboardSvc: dashService,
}
sc.folder = createFolder(t, sc, "ScenarioFolder", folderSvc)
+40 -124
View File
@@ -7,43 +7,27 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/slugify"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/client"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
@@ -93,7 +77,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) {
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash)
dashInDB := createDashboard(t, sc, &dash)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.NoError(t, err)
@@ -191,7 +175,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) {
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash)
dashInDB := createDashboard(t, sc, &dash)
err = sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.NoError(t, err)
@@ -237,7 +221,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) {
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash)
dashInDB := createDashboard(t, sc, &dash)
err := sc.service.ConnectLibraryPanelsForDashboard(sc.ctx, sc.user, dashInDB)
require.EqualError(t, err, errLibraryPanelHeaderUIDMissing.Error())
@@ -293,7 +277,7 @@ func TestIntegrationConnectLibraryPanelsForDashboard(t *testing.T) {
Title: "Testing ConnectLibraryPanelsForDashboard",
Data: simplejson.NewFromAny(dashJSON),
}
dashInDB := createDashboard(t, sc.sqlStore, sc.user, &dash)
dashInDB := createDashboard(t, sc, &dash)
err = sc.elementService.ConnectElementsToDashboard(sc.ctx, sc.user, []string{sc.initialResult.Result.UID}, dashInDB.ID)
require.NoError(t, err)
@@ -411,7 +395,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) {
element, err := sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: missingUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
var expected = getExpected(t, element, missingUID, missingName, missingModel)
var expected = getExpected(t, element, missingUID, missingName, missingModel, "Test Folder")
var result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -452,7 +436,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) {
element, err := sc.elementService.GetElement(sc.ctx, sc.user,
model.GetLibraryElementCommand{UID: existingUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model)
var expected = getExpected(t, element, existingUID, existingName, sc.initialResult.Result.Model, "Test Folder")
expected.FolderUID = sc.initialResult.Result.FolderUID
expected.Description = sc.initialResult.Result.Description
expected.Meta.FolderUID = sc.folder.UID
@@ -567,7 +551,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) {
element, err := sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: outsideUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
expected := getExpected(t, element, outsideUID, outsideName, outsideModel)
expected := getExpected(t, element, outsideUID, outsideName, outsideModel, "Test Folder")
result := toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -575,7 +559,7 @@ func TestIntegrationImportLibraryPanelsForDashboard(t *testing.T) {
element, err = sc.elementService.GetElement(sc.ctx, sc.user, model.GetLibraryElementCommand{UID: insideUID, FolderName: dashboards.RootFolderName})
require.NoError(t, err)
expected = getExpected(t, element, insideUID, insideName, insideModel)
expected = getExpected(t, element, insideUID, insideName, insideModel, "Test Folder")
result = toLibraryElement(t, element)
if diff := cmp.Diff(expected, result, getCompareOptions()...); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
@@ -648,6 +632,8 @@ type scenarioContext struct {
initialResult libraryPanelResult
sqlStore db.DB
lps LibraryPanelService
mockDashboard *dashboards.FakeDashboardService
mockFolder *foldertest.FakeService
}
func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement {
@@ -685,7 +671,7 @@ func toLibraryElement(t *testing.T, res model.LibraryElementDTO) libraryElement
}
}
func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name string, lEModel map[string]any) libraryElement {
func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name string, lEModel map[string]any, folderName string) libraryElement {
marshalled, err := json.Marshal(lEModel)
require.NoError(t, err)
var libModel libraryElementModel
@@ -704,7 +690,7 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str
Model: libModel,
Version: 1,
Meta: model.LibraryElementDTOMeta{
FolderName: "General",
FolderName: folderName,
FolderUID: res.FolderUID,
ConnectedDashboards: 0,
Created: res.Meta.Created,
@@ -722,59 +708,14 @@ func getExpected(t *testing.T, res model.LibraryElementDTO, UID string, name str
},
}
}
func createDashboard(t *testing.T, sc scenarioContext, dash *dashboards.Dashboard) *dashboards.Dashboard {
dash.ID = 1
dash.UID = "test-dashboard-uid"
dash.Created = time.Now()
dash.Updated = time.Now()
dash.Version = 1
func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash *dashboards.Dashboard) *dashboards.Dashboard {
dashItem := &dashboards.SaveDashboardDTO{
Dashboard: dash,
Message: "",
OrgID: user.OrgID,
User: user,
Overwrite: false,
}
features := featuremgmt.WithFeatures()
cfg := setting.NewCfg()
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
ac := actest.FakeAccessControl{ExpectedEvaluate: true}
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
dashPermissionService := acmock.NewMockedPermissionsService()
dashPermissionService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
service, err := dashboardservice.ProvideDashboardServiceImpl(
cfg, dashboardStore, folderStore,
features, acmock.NewMockedPermissionsService(), ac, actest.FakeService{}, foldertest.NewFakeService(),
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore())
require.NoError(t, err)
service.RegisterDashboardPermissions(dashPermissionService)
dashboard, err := service.SaveDashboard(context.Background(), dashItem, true)
require.NoError(t, err)
return dashboard
}
func createFolder(t *testing.T, sc scenarioContext, title string) *folder.Folder {
t.Helper()
features := featuremgmt.WithFeatures()
ac := actest.FakeAccessControl{ExpectedEvaluate: true}
cfg := setting.NewCfg()
dashboardStore, err := database.ProvideDashboardStore(sc.sqlStore, cfg, features, tagimpl.ProvideService(sc.sqlStore))
require.NoError(t, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sc.sqlStore)
fStore := folderimpl.ProvideStore(sc.sqlStore)
s := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sc.sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
t.Logf("Creating folder with title and UID %q", title)
ctx := identity.WithRequester(context.Background(), sc.user)
folder, err := s.Create(ctx, &folder.CreateFolderCommand{OrgID: sc.user.OrgID, Title: title, UID: title, SignedInUser: sc.user})
require.NoError(t, err)
return folder
return dash
}
func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
@@ -821,8 +762,6 @@ func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, s
})
}
// testScenario is a wrapper around t.Run performing common setup for library panel tests.
// It takes your real test function as a callback.
func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) {
t.Helper()
@@ -832,38 +771,29 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
sqlStore, cfg := db.InitTestDBWithCfg(t)
quotaService := quotatest.New(false, nil)
features := featuremgmt.WithFeatures()
ac := actest.FakeAccessControl{ExpectedEvaluate: true}
dashStore := &dashboards.FakeDashboardStore{}
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
dashPermissionService := acmock.NewMockedPermissionsService()
folderSvc := foldertest.NewFakeService()
folderSvc.ExpectedFolder = &folder.Folder{ID: 1}
dashService, err := dashboardservice.ProvideDashboardServiceImpl(
cfg, dashStore, folderStore,
features, acmock.NewMockedPermissionsService(), ac, actest.FakeService{}, folderSvc,
nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore())
require.NoError(t, err)
dashService.RegisterDashboardPermissions(dashPermissionService)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
fStore := folderimpl.ProvideStore(sqlStore)
folderService := folderimpl.ProvideService(
fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), folderService, features, ac, dashService, nil, nil)
mockDashboardService := dashboards.NewFakeDashboardService(t)
mockFolderService := foldertest.NewFakeService()
mockFolder := &folder.Folder{
ID: 1,
UID: "test-folder-uid",
Title: "Test Folder",
URL: "/dashboards/f/test-folder-uid/test-folder",
Version: 0,
Created: time.Now(),
Updated: time.Now(),
UpdatedBy: 0,
CreatedBy: 0,
HasACL: false,
}
mockFolderService.ExpectedFolder = mockFolder
elementService := libraryelements.ProvideService(cfg, sqlStore, routing.NewRouteRegister(), mockFolderService, features, ac, mockDashboardService, nil, nil)
service := LibraryPanelService{
Cfg: cfg,
SQLStore: sqlStore,
LibraryElementService: elementService,
FolderService: folderService,
FolderService: mockFolderService,
}
usr := &user.SignedInUser{
UserID: 1,
Name: "Signed In User",
@@ -872,17 +802,12 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
OrgID: orgID,
OrgRole: role,
LastSeenAt: time.Now(),
// Allow the user to create folders
Permissions: map[int64]map[string][]string{
orgID: {
dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll},
},
},
}
// deliberate difference between signed in user and user in db to make it crystal clear
// what to expect in the tests
// In the real world these are identical
cmd := user.CreateUserCommand{
Email: "user.in.db@test.com",
Name: "User In DB",
@@ -898,6 +823,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
require.NoError(t, err)
_, err = usrSvc.Create(context.Background(), &cmd)
require.NoError(t, err)
sc := scenarioContext{
user: usr,
ctx: ctx,
@@ -905,21 +831,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
elementService: elementService,
sqlStore: sqlStore,
lps: service,
mockDashboard: mockDashboardService,
mockFolder: mockFolderService,
}
foldr := createFolder(t, sc, "ScenarioFolder")
sc.folder = &folder.Folder{
ID: foldr.ID, // nolint:staticcheck
UID: foldr.UID,
Title: foldr.Title,
URL: dashboards.GetFolderURL(foldr.UID, slugify.Slugify(foldr.Title)),
Version: 0,
Created: foldr.Created,
Updated: foldr.Updated,
UpdatedBy: 0,
CreatedBy: 0,
HasACL: false,
}
sc.folder = mockFolder
fn(t, sc)
})
}
@@ -571,6 +571,7 @@ func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
folderService.AddFolder(fldr)
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
// Create rules in both folders
@@ -669,6 +670,8 @@ func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
fldr2 := randFolder()
fldr2.ParentUID = ""
folderService.ExpectedFolders = []*folder.Folder{fldr, fldr2}
folderService.AddFolder(fldr)
folderService.AddFolder(fldr2)
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr, fldr2)
// Create a Grafana rule for each Prometheus rule
@@ -798,6 +801,7 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
// Create a folder in the root
fldr := randFolder()
fldr.ParentUID = ""
folderService.AddFolder(fldr)
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
@@ -29,14 +29,10 @@ import (
"github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/apiserver"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
@@ -47,14 +43,10 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/secrets"
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -353,14 +345,6 @@ func TestIntegrationProvisioningApi(t *testing.T) {
orgID := int64(2)
rule := createTestAlertRule("rule", orgID)
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: rule.FolderUID,
Title: "Folder Title",
OrgID: orgID,
SignedInUser: &user.SignedInUser{OrgID: orgID},
})
require.NoError(t, err)
insertRuleInOrg(t, sut, rule, orgID)
rule.FolderUID = "does-not-exist"
@@ -462,14 +446,7 @@ func TestIntegrationProvisioningApi(t *testing.T) {
rc.Req.Header = map[string][]string{"X-Disable-Provenance": {"true"}}
rc.OrgID = 3
rule := createTestAlertRule("rule", 1)
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: "folder-uid",
Title: "Folder Title",
OrgID: rc.OrgID,
SignedInUser: &user.SignedInUser{OrgID: rc.OrgID},
})
require.NoError(t, err)
rule.FolderUID = "folder-uid3"
response := sut.RoutePostAlertRule(&rc, rule)
@@ -486,13 +463,7 @@ func TestIntegrationProvisioningApi(t *testing.T) {
rule.UID = uid
orgID := int64(3)
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: "folder-uid",
Title: "Folder Title",
OrgID: orgID,
SignedInUser: &user.SignedInUser{OrgID: orgID},
})
require.NoError(t, err)
rule.FolderUID = "folder-uid3"
insertRuleInOrg(t, sut, rule, orgID)
rc := createTestRequestCtx()
@@ -560,14 +531,7 @@ func TestIntegrationProvisioningApi(t *testing.T) {
uid := util.GenerateShortUID()
rule := createTestAlertRule("rule", 3)
rule.UID = uid
_, err := sut.folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: rule.FolderUID,
Title: "Folder Title",
OrgID: rule.OrgID,
SignedInUser: &user.SignedInUser{OrgID: rule.OrgID},
})
require.NoError(t, err)
rule.FolderUID = "folder-uid3"
insertRuleInOrg(t, sut, rule, 3)
@@ -2053,7 +2017,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
GetsConfig(models.AlertConfiguration{
AlertmanagerConfiguration: string(raw),
})
sqlStore, cfg := db.InitTestDBWithCfg(t)
sqlStore, _ := db.InitTestDBWithCfg(t)
quotas := &provisioning.MockQuotaChecker{}
quotas.EXPECT().LimitOK()
@@ -2077,14 +2041,39 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
}}, nil).Maybe()
ac := &recordingAccessControlFake{}
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore))
require.NoError(t, err)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
fStore := folderimpl.ProvideStore(sqlStore)
folderService := folderimpl.ProvideService(
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashboardStore, folderStore,
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
folderService := foldertest.NewFakeService()
folder1 := &folder.Folder{
UID: "folder-uid",
Title: "Folder Title",
Fullpath: "Folder Title",
OrgID: 1,
}
folder2 := &folder.Folder{
UID: "folder-uid2",
Title: "Folder Title2",
ParentUID: "folder-uid",
Fullpath: "Folder Title2",
OrgID: 1,
}
folder3 := &folder.Folder{
UID: "folder-uid3",
Title: "Folder Title3",
ParentUID: "folder-uid",
Fullpath: "Folder Title3",
OrgID: 3,
}
folderService.SetFolders(map[string]*folder.Folder{
"folder-uid": folder1,
"folder-uid2": folder2,
"folder-uid3": folder3,
})
folderService.ExpectedFolders = []*folder.Folder{
folder1,
folder2,
folder3,
}
// if not one of the two above, return ErrFolderNotFound
folderService.ExpectedError = dashboards.ErrFolderNotFound
store := store.DBstore{
Logger: log,
SQLStore: sqlStore,
@@ -2104,23 +2093,6 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment {
*/
}
parent, err := folderService.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: 1,
UID: "folder-uid",
Title: "Folder Title",
SignedInUser: user,
})
require.NoError(t, err)
_, err = folderService.Create(context.Background(), &folder.CreateFolderCommand{
OrgID: 1,
UID: "folder-uid2",
Title: "Folder Title2",
ParentUID: parent.UID,
SignedInUser: user,
})
require.NoError(t, err)
ruleAuthz := &fakes.FakeRuleService{}
features := featuremgmt.WithFeatures()
+39 -30
View File
@@ -27,12 +27,12 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/infra/db"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@@ -242,10 +242,10 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
}
sqlStore := db.InitTestDB(t)
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
fakeFolderService := foldertest.NewFakeService()
b := &fakeBus{}
logger := &logtest.Fake{}
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
store := createTestStore(sqlStore, fakeFolderService, logger, cfg.UnifiedAlerting, b)
store.FeatureToggles = featuremgmt.WithFeatures()
gen := models.RuleGen
@@ -258,15 +258,38 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
parentFolderUid := uuid.NewString()
parentFolderTitle := "Very Parent Folder"
createFolder(t, store, parentFolderUid, parentFolderTitle, rule1.OrgID, "")
rule1FolderTitle := "folder-" + rule1.Title
rule2FolderTitle := "folder-" + rule2.Title
rule3FolderTitle := "folder-" + rule3.Title
createFolder(t, store, rule1.NamespaceUID, rule1FolderTitle, rule1.OrgID, parentFolderUid)
createFolder(t, store, rule2.NamespaceUID, rule2FolderTitle, rule2.OrgID, "")
createFolder(t, store, rule3.NamespaceUID, rule3FolderTitle, rule3.OrgID, "")
createFolder(t, store, rule2.NamespaceUID, "same UID folder", gen.GenerateRef().OrgID, "") // create a folder with the same UID but in the different org
fakeFolderService.AddFolder(&folder.Folder{
UID: rule1.NamespaceUID,
Title: rule1FolderTitle,
OrgID: rule1.OrgID,
ParentUID: parentFolderUid,
Fullpath: rule1FolderTitle,
})
fakeFolderService.AddFolder(&folder.Folder{
UID: rule2.NamespaceUID,
Title: rule2FolderTitle,
OrgID: rule2.OrgID,
ParentUID: "",
Fullpath: rule2FolderTitle,
})
fakeFolderService.AddFolder(&folder.Folder{
UID: rule3.NamespaceUID,
Title: rule3FolderTitle,
OrgID: rule3.OrgID,
ParentUID: "",
Fullpath: rule3FolderTitle,
})
fakeFolderService.AddFolder(&folder.Folder{
UID: parentFolderUid,
Title: parentFolderTitle,
OrgID: rule1.OrgID,
ParentUID: "",
Fullpath: parentFolderTitle,
})
tc := []struct {
name string
@@ -343,7 +366,14 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
}
t.Run("when nested folders are enabled folders should contain full path", func(t *testing.T) {
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
fakeFolderService.AddFolder(&folder.Folder{
UID: rule1.NamespaceUID,
Title: rule1FolderTitle,
OrgID: rule1.OrgID,
ParentUID: parentFolderUid,
Fullpath: parentFolderTitle + "/" + rule1FolderTitle,
})
query := &models.GetAlertRulesForSchedulingQuery{
PopulateFolders: true,
}
@@ -1597,27 +1627,6 @@ func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerat
return rule
}
func createFolder(t *testing.T, store *DBstore, uid, title string, orgID int64, parentUID string) {
t.Helper()
u := &user.SignedInUser{
UserID: 1,
OrgID: orgID,
OrgRole: org.RoleAdmin,
IsGrafanaAdmin: true,
}
_, err := store.FolderService.Create(context.Background(), &folder.CreateFolderCommand{
UID: uid,
OrgID: orgID,
Title: title,
Description: "",
SignedInUser: u,
ParentUID: parentUID,
})
require.NoError(t, err)
}
func setupFolderService(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles) folder.Service {
tracer := tracing.InitializeTracerForTest()
inProcBus := bus.ProvideBus(tracer)
+6 -11
View File
@@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/org"
@@ -28,7 +27,7 @@ func TestIntegration_GetUserVisibleNamespaces(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
folderService := foldertest.NewFakeService()
b := &fakeBus{}
logger := log.New("test-dbstore")
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
@@ -40,18 +39,14 @@ func TestIntegration_GetUserVisibleNamespaces(t *testing.T) {
IsGrafanaAdmin: true,
}
folders := []struct {
uid string
title string
parentUid string
}{
{uid: uuid.NewString(), title: "folder1", parentUid: ""},
{uid: uuid.NewString(), title: "folder2", parentUid: ""},
{uid: uuid.NewString(), title: "nested/folder", parentUid: ""},
folders := []*folder.Folder{
{UID: uuid.NewString(), Title: "folder1", ParentUID: "", OrgID: 1},
{UID: uuid.NewString(), Title: "folder2", ParentUID: "", OrgID: 1},
{UID: uuid.NewString(), Title: "nested/folder", ParentUID: "", OrgID: 1},
}
for _, f := range folders {
createFolder(t, store, f.uid, f.title, 1, f.parentUid)
folderService.AddFolder(f)
}
t.Run("returns all folders", func(t *testing.T) {
+3 -5
View File
@@ -24,13 +24,12 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
ngalertfakes "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
"github.com/grafana/grafana/pkg/services/ngalert/testutil"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
@@ -83,9 +82,8 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration, opts ...TestEnvOpti
tracer := tracing.InitializeTracerForTest()
bus := bus.ProvideBus(tracer)
folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore)
dashboardService, dashboardStore := testutil.SetupDashboardService(tb, sqlStore, folderStore, cfg)
folderService := testutil.SetupFolderService(tb, cfg, sqlStore, dashboardStore, folderStore, bus, options.featureToggles, ac)
folderService := foldertest.NewFakeService()
dashboardService := dashboards.NewFakeDashboardService(tb)
ruleStore, err := store.ProvideDBStore(cfg, options.featureToggles, sqlStore, folderService, &dashboards.FakeDashboardService{}, ac, bus)
require.NoError(tb, err)
ng, err := ngalert.ProvideService(
+3 -1
View File
@@ -504,8 +504,10 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
folderSvc := folderimpl.ProvideService(
fStore, acmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore,
nil, sqlStore, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
orgService, err := orgimpl.ProvideService(sqlStore, cfg, quotaService)
require.NoError(t, err)
dashService, err := dashService.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), acmock.NewMockedPermissionsService(),
ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
ac, actest.FakeService{}, folderSvc, nil, client.MockTestRestConfig{}, nil, quotaService, orgService, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService(),
serverlock.ProvideService(sqlStore, tracing.InitializeTracerForTest()),
kvstore.NewFakeKVStore())
require.NoError(t, err)
@@ -10,13 +10,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/dashboards/database"
@@ -25,14 +21,11 @@ import (
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/search/sort"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
@@ -826,31 +819,42 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol
// dashboard store commands that should be called.
dashStore, err := database.ProvideDashboardStore(db, cfg, features, tagimpl.ProvideService(db))
require.NoError(t, err)
fStore := folderimpl.ProvideStore(db)
folderSvc := folderimpl.ProvideService(
fStore, actest.FakeAccessControl{ExpectedEvaluate: true}, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderimpl.ProvideDashboardFolderStore(db),
nil, db, features, supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService(), apiserver.WithoutRestConfig)
// create parent folder
parent, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: "parent",
OrgID: orgID,
// create in both the folder & dashboard tables
parent, err := fStore.Create(context.Background(), folder.CreateFolderCommand{
Title: "parent",
SignedInUser: usr,
})
require.NoError(t, err)
// create subfolder
subfolder, err := folderSvc.Create(context.Background(), &folder.CreateFolderCommand{
UID: "subfolder",
ParentUID: "parent",
OrgID: orgID,
Title: "subfolder",
UID: "parent",
SignedInUser: usr,
})
require.NoError(t, err)
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: orgID,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": "parent",
"uid": parent.UID,
}),
IsFolder: true,
})
require.NoError(t, err)
subfolder, err := fStore.Create(context.Background(), folder.CreateFolderCommand{
Title: "subfolder",
OrgID: orgID,
UID: "subfolder",
ParentUID: parent.UID,
SignedInUser: usr,
})
require.NoError(t, err)
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: orgID,
FolderUID: parent.UID,
Dashboard: simplejson.NewFromAny(map[string]any{
"title": "subfolder",
"uid": subfolder.UID,
}),
IsFolder: true,
})
require.NoError(t, err)
// create a root level dashboard
_, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{
OrgID: orgID,
-5
View File
@@ -158,11 +158,6 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS
if ss.IsUnifiedAlertingEnabled() {
sb.Write(`(SELECT COUNT(DISTINCT (` + dialect.Quote("rule_group") + `)) FROM ` + dialect.Quote("alert_rule") + `) AS rule_groups,`)
}
// currently not supported when dashboards are in unified storage
if !ss.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
sb.Write(`(SELECT SUM(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_total,`, dialect.BooleanValue(false))
sb.Write(`(SELECT MAX(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_max,`, dialect.BooleanValue(false))
}
sb.Write(ss.roleCounterSQL(ctx))