Dashboards: Add Dashboard API Validation tests and fix underlying issues (#103502)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/search/model"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// DashboardService is a service for operating on dashboards.
|
||||
@@ -33,6 +34,10 @@ type DashboardService interface {
|
||||
GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error)
|
||||
CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error
|
||||
CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error)
|
||||
SetDefaultPermissions(ctx context.Context, dto *SaveDashboardDTO, dash *Dashboard, provisioned bool)
|
||||
UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*Dashboard, error)
|
||||
ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error
|
||||
ValidateBasicDashboardProperties(title string, uid string, message string) error
|
||||
}
|
||||
|
||||
type PermissionsRegistrationService interface {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.3. DO NOT EDIT.
|
||||
|
||||
package dashboards
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/grafana/grafana/pkg/services/search/model"
|
||||
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// FakeDashboardService is an autogenerated mock type for the DashboardService type
|
||||
@@ -46,6 +48,24 @@ func (_m *FakeDashboardService) BuildSaveDashboardCommand(ctx context.Context, d
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CleanUpDashboard provides a mock function with given fields: ctx, dashboardUID, orgId
|
||||
func (_m *FakeDashboardService) CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error {
|
||||
ret := _m.Called(ctx, dashboardUID, orgId)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CleanUpDashboard")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok {
|
||||
r0 = rf(ctx, dashboardUID, orgId)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CountDashboardsInOrg provides a mock function with given fields: ctx, orgID
|
||||
func (_m *FakeDashboardService) CountDashboardsInOrg(ctx context.Context, orgID int64) (int64, error) {
|
||||
ret := _m.Called(ctx, orgID)
|
||||
@@ -438,17 +458,70 @@ func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *Fin
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CleanUpDashboard provides a mock function with given fields: ctx, dashboardUID, orgId
|
||||
func (_m *FakeDashboardService) CleanUpDashboard(ctx context.Context, dashboardUID string, orgId int64) error {
|
||||
ret := _m.Called(ctx, dashboardUID, orgId)
|
||||
// SetDefaultPermissions provides a mock function with given fields: ctx, dto, dash, provisioned
|
||||
func (_m *FakeDashboardService) SetDefaultPermissions(ctx context.Context, dto *SaveDashboardDTO, dash *Dashboard, provisioned bool) {
|
||||
_m.Called(ctx, dto, dash, provisioned)
|
||||
}
|
||||
|
||||
// UnstructuredToLegacyDashboard provides a mock function with given fields: ctx, item, orgID
|
||||
func (_m *FakeDashboardService) UnstructuredToLegacyDashboard(ctx context.Context, item *unstructured.Unstructured, orgID int64) (*Dashboard, error) {
|
||||
ret := _m.Called(ctx, item, orgID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CleanUpDashboard")
|
||||
panic("no return value specified for UnstructuredToLegacyDashboard")
|
||||
}
|
||||
|
||||
var r0 *Dashboard
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, int64) (*Dashboard, error)); ok {
|
||||
return rf(ctx, item, orgID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *unstructured.Unstructured, int64) *Dashboard); ok {
|
||||
r0 = rf(ctx, item, orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*Dashboard)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *unstructured.Unstructured, int64) error); ok {
|
||||
r1 = rf(ctx, item, orgID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ValidateBasicDashboardProperties provides a mock function with given fields: title, uid, message
|
||||
func (_m *FakeDashboardService) ValidateBasicDashboardProperties(title string, uid string, message string) error {
|
||||
ret := _m.Called(title, uid, message)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ValidateBasicDashboardProperties")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, int64) error); ok {
|
||||
r0 = rf(ctx, dashboardUID, orgId)
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(title, uid, message)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ValidateDashboardRefreshInterval provides a mock function with given fields: minRefreshInterval, targetRefreshInterval
|
||||
func (_m *FakeDashboardService) ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error {
|
||||
ret := _m.Called(minRefreshInterval, targetRefreshInterval)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ValidateDashboardRefreshInterval")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(minRefreshInterval, targetRefreshInterval)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
@@ -70,22 +70,98 @@ func (d *dashboardStore) emitEntityEvent() bool {
|
||||
return d.features != nil && d.features.IsEnabledGlobally(featuremgmt.FlagPanelTitleSearch)
|
||||
}
|
||||
|
||||
// TODO: once the folder service removes usage of this function, remove it here. The dashboard service now implements this
|
||||
// on the service level for dashboards.
|
||||
func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *dashboards.Dashboard, overwrite bool) (bool, error) {
|
||||
func (d *dashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dash *dashboards.Dashboard, overwrite bool) (bool, error) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.database.ValidateDashboardBeforesave")
|
||||
defer span.End()
|
||||
|
||||
isParentFolderChanged := false
|
||||
err := d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
var err error
|
||||
isParentFolderChanged, err = getExistingDashboardByIDOrUIDForUpdate(sess, dashboard, overwrite)
|
||||
if err != nil {
|
||||
return err
|
||||
dashWithIdExists := false
|
||||
var existingById dashboards.Dashboard
|
||||
|
||||
// we don't save FolderID in kubernetes object when saving through k8s
|
||||
// this block guarantees we save dashboards with folder_id and folder_uid in those cases
|
||||
if !dash.IsFolder && dash.FolderUID != "" && dash.FolderID == 0 { // nolint:staticcheck
|
||||
var existing dashboards.Dashboard
|
||||
folderIdFound, err := sess.Where("uid=? AND org_id=?", dash.FolderUID, dash.OrgID).Get(&existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if folderIdFound {
|
||||
dash.FolderID = existing.ID // nolint:staticcheck
|
||||
}
|
||||
}
|
||||
|
||||
if dash.ID > 0 {
|
||||
var err error
|
||||
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existingById)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err)
|
||||
}
|
||||
|
||||
if !dashWithIdExists {
|
||||
return dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
if dash.UID == "" {
|
||||
dash.SetUID(existingById.UID)
|
||||
}
|
||||
}
|
||||
|
||||
dashWithUidExists := false
|
||||
var existingByUid dashboards.Dashboard
|
||||
|
||||
if dash.UID != "" {
|
||||
var err error
|
||||
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgID, dash.UID).Get(&existingByUid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !dashWithIdExists && !dashWithUidExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dashWithIdExists && dashWithUidExists && existingById.ID != existingByUid.ID {
|
||||
return dashboards.ErrDashboardWithSameUIDExists
|
||||
}
|
||||
|
||||
existing := existingById
|
||||
|
||||
if !dashWithIdExists && dashWithUidExists {
|
||||
dash.SetID(existingByUid.ID)
|
||||
dash.SetUID(existingByUid.UID)
|
||||
existing = existingByUid
|
||||
}
|
||||
|
||||
if (existing.IsFolder && !dash.IsFolder) ||
|
||||
(!existing.IsFolder && dash.IsFolder) {
|
||||
return dashboards.ErrDashboardTypeMismatch
|
||||
}
|
||||
|
||||
if !dash.IsFolder && dash.FolderUID != existing.FolderUID {
|
||||
isParentFolderChanged = true
|
||||
}
|
||||
|
||||
// check for is someone else has written in between
|
||||
if dash.Version != existing.Version {
|
||||
if overwrite {
|
||||
dash.SetVersion(existing.Version)
|
||||
} else {
|
||||
return dashboards.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginID != "" && !overwrite {
|
||||
return dashboards.UpdatePluginDashboardError{PluginId: existing.PluginID}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -186,7 +262,7 @@ func (d *dashboardStore) SaveProvisionedDashboard(ctx context.Context, cmd dashb
|
||||
var result *dashboards.Dashboard
|
||||
var err error
|
||||
err = d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
result, err = saveDashboard(sess, &cmd, d.emitEntityEvent())
|
||||
result, err = d.saveDashboard(ctx, sess, &cmd, d.emitEntityEvent())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,7 +283,7 @@ func (d *dashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveD
|
||||
var result *dashboards.Dashboard
|
||||
var err error
|
||||
err = d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
|
||||
result, err = saveDashboard(sess, &cmd, d.emitEntityEvent())
|
||||
result, err = d.saveDashboard(ctx, sess, &cmd, d.emitEntityEvent())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -324,126 +400,16 @@ func (d *dashboardStore) CountInOrg(ctx context.Context, orgID int64, isFolder b
|
||||
return r.Count, nil
|
||||
}
|
||||
|
||||
func getExistingDashboardByIDOrUIDForUpdate(sess *db.Session, dash *dashboards.Dashboard, overwrite bool) (bool, error) {
|
||||
dashWithIdExists := false
|
||||
isParentFolderChanged := false
|
||||
var existingById dashboards.Dashboard
|
||||
|
||||
if dash.ID > 0 {
|
||||
var err error
|
||||
dashWithIdExists, err = sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existingById)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("SQL query for existing dashboard by ID failed: %w", err)
|
||||
}
|
||||
|
||||
if !dashWithIdExists {
|
||||
return false, dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
if dash.UID == "" {
|
||||
dash.SetUID(existingById.UID)
|
||||
}
|
||||
}
|
||||
|
||||
dashWithUidExists := false
|
||||
var existingByUid dashboards.Dashboard
|
||||
|
||||
if dash.UID != "" {
|
||||
var err error
|
||||
dashWithUidExists, err = sess.Where("org_id=? AND uid=?", dash.OrgID, dash.UID).Get(&existingByUid)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("SQL query for existing dashboard by UID failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !dashWithIdExists && !dashWithUidExists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if dashWithIdExists && dashWithUidExists && existingById.ID != existingByUid.ID {
|
||||
return false, dashboards.ErrDashboardWithSameUIDExists
|
||||
}
|
||||
|
||||
existing := existingById
|
||||
|
||||
if !dashWithIdExists && dashWithUidExists {
|
||||
dash.SetID(existingByUid.ID)
|
||||
dash.SetUID(existingByUid.UID)
|
||||
existing = existingByUid
|
||||
}
|
||||
|
||||
if (existing.IsFolder && !dash.IsFolder) ||
|
||||
(!existing.IsFolder && dash.IsFolder) {
|
||||
return isParentFolderChanged, dashboards.ErrDashboardTypeMismatch
|
||||
}
|
||||
|
||||
if !dash.IsFolder && dash.FolderUID != existing.FolderUID {
|
||||
isParentFolderChanged = true
|
||||
}
|
||||
|
||||
// check for is someone else has written in between
|
||||
if dash.Version != existing.Version {
|
||||
if overwrite {
|
||||
dash.SetVersion(existing.Version)
|
||||
} else {
|
||||
return isParentFolderChanged, dashboards.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginID != "" && !overwrite {
|
||||
return isParentFolderChanged, dashboards.UpdatePluginDashboardError{PluginId: existing.PluginID}
|
||||
}
|
||||
|
||||
return isParentFolderChanged, nil
|
||||
}
|
||||
|
||||
func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitEntityEvent bool) (*dashboards.Dashboard, error) {
|
||||
func (d *dashboardStore) saveDashboard(ctx context.Context, sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitEntityEvent bool) (*dashboards.Dashboard, error) {
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
userId := cmd.UserID
|
||||
|
||||
if userId == 0 {
|
||||
userId = -1
|
||||
isParentFolderChanged, err := d.ValidateDashboardBeforeSave(ctx, dash, cmd.Overwrite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we don't save FolderID in kubernetes object when saving through k8s
|
||||
// this block guarantees we save dashboards with folder_id and folder_uid in those cases
|
||||
if !dash.IsFolder && dash.FolderUID != "" && dash.FolderID == 0 { // nolint:staticcheck
|
||||
var existing dashboards.Dashboard
|
||||
folderIdFound, err := sess.Where("uid=? AND org_id=?", dash.FolderUID, dash.OrgID).Get(&existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if folderIdFound {
|
||||
dash.FolderID = existing.ID // nolint:staticcheck
|
||||
}
|
||||
}
|
||||
|
||||
if dash.ID > 0 {
|
||||
var existing dashboards.Dashboard
|
||||
dashWithIdExists, err := sess.Where("id=? AND org_id=?", dash.ID, dash.OrgID).Get(&existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !dashWithIdExists {
|
||||
return nil, dashboards.ErrDashboardNotFound
|
||||
}
|
||||
|
||||
// check for is someone else has written in between
|
||||
if dash.Version != existing.Version {
|
||||
if cmd.Overwrite {
|
||||
dash.SetVersion(existing.Version)
|
||||
} else {
|
||||
return nil, dashboards.ErrDashboardVersionMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// do not allow plugin dashboard updates without overwrite flag
|
||||
if existing.PluginID != "" && !cmd.Overwrite {
|
||||
return nil, dashboards.UpdatePluginDashboardError{PluginId: existing.PluginID}
|
||||
}
|
||||
if isParentFolderChanged {
|
||||
d.log.Debug("Dashboard parent folder has changed", "dashboard", dash.UID, "newFolder", dash.FolderUID)
|
||||
}
|
||||
|
||||
if dash.UID == "" {
|
||||
@@ -452,14 +418,12 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE
|
||||
|
||||
parentVersion := dash.Version
|
||||
var affectedRows int64
|
||||
var err error
|
||||
|
||||
if dash.ID == 0 {
|
||||
dash.SetVersion(1)
|
||||
dash.Created = time.Now()
|
||||
dash.CreatedBy = userId
|
||||
dash.CreatedBy = dash.UpdatedBy
|
||||
dash.Updated = time.Now()
|
||||
dash.UpdatedBy = userId
|
||||
metrics.MApiDashboardInsert.Inc()
|
||||
affectedRows, err = sess.Nullable("folder_uid").Insert(dash)
|
||||
} else {
|
||||
@@ -471,8 +435,6 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE
|
||||
dash.Updated = time.Now()
|
||||
}
|
||||
|
||||
dash.UpdatedBy = userId
|
||||
|
||||
affectedRows, err = sess.MustCols("folder_id", "folder_uid").Nullable("folder_uid").ID(dash.ID).Update(dash)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,11 @@ var (
|
||||
StatusCode: 400,
|
||||
Status: "bad-request",
|
||||
}
|
||||
ErrQuotaReached = dashboardaccess.DashboardErr{
|
||||
Reason: "Dashboard quota reached",
|
||||
StatusCode: 403,
|
||||
Status: "quota-reached",
|
||||
}
|
||||
|
||||
ErrFolderNotFound = errors.New("folder not found")
|
||||
ErrFolderVersionMismatch = errors.New("the folder has been changed by someone else")
|
||||
|
||||
@@ -608,6 +608,29 @@ func (dr *DashboardServiceImpl) GetProvisionedDashboardDataByDashboardUID(ctx co
|
||||
return dr.dashboardStore.GetProvisionedDataByDashboardUID(ctx, orgID, dashboardUID)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) ValidateBasicDashboardProperties(title string, uid string, message string) error {
|
||||
if title == "" {
|
||||
return dashboards.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
if len(title) > 5000 {
|
||||
return dashboards.ErrDashboardTitleTooLong
|
||||
}
|
||||
|
||||
// Validate message
|
||||
if message != "" && len(message) > 500 {
|
||||
return dashboards.ErrDashboardMessageTooLong
|
||||
}
|
||||
|
||||
if !util.IsValidShortUID(uid) {
|
||||
return dashboards.ErrDashboardInvalidUid
|
||||
} else if util.IsShortUIDTooLong(uid) {
|
||||
return dashboards.ErrDashboardUidTooLong
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, dto *dashboards.SaveDashboardDTO,
|
||||
validateProvisionedDashboard bool) (*dashboards.SaveDashboardCommand, error) {
|
||||
@@ -621,16 +644,8 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
|
||||
dash.Data.Set("title", dash.Title)
|
||||
dash.SetUID(strings.TrimSpace(dash.UID))
|
||||
|
||||
if dash.Title == "" {
|
||||
return nil, dashboards.ErrDashboardTitleEmpty
|
||||
}
|
||||
|
||||
if len(dash.Title) > 5000 {
|
||||
return nil, dashboards.ErrDashboardTitleTooLong
|
||||
}
|
||||
|
||||
if len(dto.Message) > 500 {
|
||||
return nil, dashboards.ErrDashboardMessageTooLong
|
||||
if err := dr.ValidateBasicDashboardProperties(dash.Title, dash.UID, dto.Message); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
|
||||
@@ -643,13 +658,7 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
|
||||
return nil, dashboards.ErrDashboardFolderNameExists
|
||||
}
|
||||
|
||||
if !util.IsValidShortUID(dash.UID) {
|
||||
return nil, dashboards.ErrDashboardInvalidUid
|
||||
} else if util.IsShortUIDTooLong(dash.UID) {
|
||||
return nil, dashboards.ErrDashboardUidTooLong
|
||||
}
|
||||
|
||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dash); err != nil {
|
||||
if err := dr.ValidateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dash.Data.Get("refresh").MustString("")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -910,13 +919,12 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.
|
||||
return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd)
|
||||
}
|
||||
|
||||
func validateDashboardRefreshInterval(minRefreshInterval string, dash *dashboards.Dashboard) error {
|
||||
func (dr *DashboardServiceImpl) ValidateDashboardRefreshInterval(minRefreshInterval string, targetRefreshInterval string) error {
|
||||
if minRefreshInterval == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
refresh := dash.Data.Get("refresh").MustString("")
|
||||
if refresh == "" || refresh == "auto" {
|
||||
if targetRefreshInterval == "" || targetRefreshInterval == "auto" {
|
||||
// since no refresh is set it is a valid refresh rate
|
||||
return nil
|
||||
}
|
||||
@@ -925,9 +933,9 @@ func validateDashboardRefreshInterval(minRefreshInterval string, dash *dashboard
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing min refresh interval %q failed: %w", minRefreshInterval, err)
|
||||
}
|
||||
d, err := gtime.ParseDuration(refresh)
|
||||
d, err := gtime.ParseDuration(targetRefreshInterval)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing refresh duration %q failed: %w", refresh, err)
|
||||
return fmt.Errorf("parsing refresh duration %q failed: %w", targetRefreshInterval, err)
|
||||
}
|
||||
|
||||
if d < minRefreshIntervalDur {
|
||||
@@ -942,7 +950,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.SaveProvisionedDashboard")
|
||||
defer span.End()
|
||||
|
||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
||||
if err := dr.ValidateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard.Data.Get("refresh").MustString("")); err != nil {
|
||||
dr.log.Warn("Changing refresh interval for provisioned dashboard to minimum refresh interval", "dashboardUid",
|
||||
dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval", dr.cfg.MinRefreshInterval)
|
||||
dto.Dashboard.Data.Set("refresh", dr.cfg.MinRefreshInterval)
|
||||
@@ -971,7 +979,7 @@ func (dr *DashboardServiceImpl) SaveProvisionedDashboard(ctx context.Context, dt
|
||||
}
|
||||
|
||||
if dto.Dashboard.ID == 0 {
|
||||
dr.setDefaultPermissions(ctx, dto, dash, true)
|
||||
dr.SetDefaultPermissions(ctx, dto, dash, true)
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
@@ -1002,7 +1010,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.SaveDashboard")
|
||||
defer span.End()
|
||||
|
||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
||||
if err := dr.ValidateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard.Data.Get("refresh").MustString("")); err != nil {
|
||||
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
||||
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title, "minRefreshInterval",
|
||||
dr.cfg.MinRefreshInterval)
|
||||
@@ -1021,7 +1029,7 @@ func (dr *DashboardServiceImpl) SaveDashboard(ctx context.Context, dto *dashboar
|
||||
|
||||
// new dashboard created
|
||||
if dto.Dashboard.ID == 0 {
|
||||
dr.setDefaultPermissions(ctx, dto, dash, false)
|
||||
dr.SetDefaultPermissions(ctx, dto, dash, false)
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
@@ -1095,7 +1103,7 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.ImportDashboard")
|
||||
defer span.End()
|
||||
|
||||
if err := validateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard); err != nil {
|
||||
if err := dr.ValidateDashboardRefreshInterval(dr.cfg.MinRefreshInterval, dto.Dashboard.Data.Get("refresh").MustString("")); err != nil {
|
||||
dr.log.Warn("Changing refresh interval for imported dashboard to minimum refresh interval",
|
||||
"dashboardUid", dto.Dashboard.UID, "dashboardTitle", dto.Dashboard.Title,
|
||||
"minRefreshInterval", dr.cfg.MinRefreshInterval)
|
||||
@@ -1112,7 +1120,7 @@ func (dr *DashboardServiceImpl) ImportDashboard(ctx context.Context, dto *dashbo
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dr.setDefaultPermissions(ctx, dto, dash, false)
|
||||
dr.SetDefaultPermissions(ctx, dto, dash, false)
|
||||
|
||||
return dash, nil
|
||||
}
|
||||
@@ -1178,7 +1186,7 @@ func (dr *DashboardServiceImpl) GetDashboardsByPluginID(ctx context.Context, que
|
||||
return dr.dashboardStore.GetDashboardsByPluginID(ctx, query)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) {
|
||||
func (dr *DashboardServiceImpl) SetDefaultPermissions(ctx context.Context, dto *dashboards.SaveDashboardDTO, dash *dashboards.Dashboard, provisioned bool) {
|
||||
ctx, span := tracer.Start(ctx, "dashboards.service.setDefaultPermissions")
|
||||
defer span.End()
|
||||
|
||||
@@ -1765,9 +1773,14 @@ func (dr *DashboardServiceImpl) saveProvisionedDashboardThroughK8s(ctx context.C
|
||||
meta.SetManagerProperties(m)
|
||||
meta.SetSourceProperties(s)
|
||||
|
||||
// Update will create if not exists (upsert!)
|
||||
out, err := dr.k8sclient.Update(ctx, obj, cmd.OrgID)
|
||||
if err != nil {
|
||||
if err != nil && apierrors.IsNotFound(err) {
|
||||
// Create if it doesn't already exist.
|
||||
out, err = dr.k8sclient.Create(ctx, obj, cmd.OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1782,9 +1795,14 @@ func (dr *DashboardServiceImpl) saveDashboardThroughK8s(ctx context.Context, cmd
|
||||
|
||||
dashboard.SetPluginIDMeta(obj, cmd.PluginID)
|
||||
|
||||
// Update will create if not exists (upsert!)
|
||||
out, err := dr.k8sclient.Update(ctx, obj, orgID)
|
||||
if err != nil {
|
||||
if err != nil && apierrors.IsNotFound(err) {
|
||||
// Create if it doesn't already exist.
|
||||
out, err = dr.k8sclient.Create(ctx, obj, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -2161,7 +2161,7 @@ func TestCountDashboardsInOrg(t *testing.T) {
|
||||
|
||||
t.Run("Should fallback to dashboard store if Kubernetes feature flags are not enabled", func(t *testing.T) {
|
||||
service.features = featuremgmt.WithFeatures()
|
||||
fakeStore.On("CountInOrg", mock.Anything, mock.Anything, false).Return(nil, nil).Once()
|
||||
fakeStore.On("CountInOrg", mock.Anything, mock.Anything, false).Return(int64(1), nil).Once()
|
||||
_, err := service.CountDashboardsInOrg(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
fakeStore.AssertExpectations(t)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Code generated by mockery v2.52.2. DO NOT EDIT.
|
||||
// Code generated by mockery v2.53.3. DO NOT EDIT.
|
||||
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
quota "github.com/grafana/grafana/pkg/services/quota"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// FakeDashboardStore is an autogenerated mock type for the Store type
|
||||
@@ -91,12 +90,12 @@ func (_m *FakeDashboardStore) CountDashboardsInFolders(ctx context.Context, requ
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountInOrg provides a mock function with given fields: ctx, orgID
|
||||
// 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)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Count")
|
||||
panic("no return value specified for CountInOrg")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
@@ -107,9 +106,7 @@ func (_m *FakeDashboardStore) CountInOrg(ctx context.Context, orgID int64, isFol
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int64, bool) int64); ok {
|
||||
r0 = rf(ctx, orgID, isFolder)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int64, bool) error); ok {
|
||||
@@ -433,7 +430,7 @@ func (_m *FakeDashboardStore) GetDashboardsByPluginID(ctx context.Context, query
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetOrphanedProvisionedDashboards provides a mock function with given fields: ctx, notIn
|
||||
// GetOrphanedProvisionedDashboards provides a mock function with given fields: ctx, notIn, orgID
|
||||
func (_m *FakeDashboardStore) GetOrphanedProvisionedDashboards(ctx context.Context, notIn []string, orgID int64) ([]*Dashboard, error) {
|
||||
ret := _m.Called(ctx, notIn, orgID)
|
||||
|
||||
@@ -493,7 +490,7 @@ func (_m *FakeDashboardStore) GetProvisionedDashboardData(ctx context.Context, n
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetProvisionedDashboardsByName provides a mock function with given fields: ctx, name
|
||||
// GetProvisionedDashboardsByName provides a mock function with given fields: ctx, name, orgID
|
||||
func (_m *FakeDashboardStore) GetProvisionedDashboardsByName(ctx context.Context, name string, orgID int64) ([]*Dashboard, error) {
|
||||
ret := _m.Called(ctx, name, orgID)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user