Dashboards: Add feature restore dashboards backend (#83131)

Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
Ezequiel Victorero
2024-05-16 14:36:26 -03:00
committed by GitHub
co-authored by Sofia Papagiannaki
parent edae5fc791
commit 42d75ac737
30 changed files with 1230 additions and 96 deletions
+92 -12
View File
@@ -7,8 +7,6 @@ import (
"strings"
"time"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
@@ -16,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -513,6 +512,67 @@ func (d *dashboardStore) GetDashboardsByPluginID(ctx context.Context, query *das
}
return dashboards, nil
}
func (d *dashboardStore) GetSoftDeletedDashboard(ctx context.Context, orgID int64, uid string) (*dashboards.Dashboard, error) {
if orgID == 0 || uid == "" {
return nil, dashboards.ErrDashboardIdentifierNotSet
}
var queryResult *dashboards.Dashboard
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
dashboard := dashboards.Dashboard{OrgID: orgID, UID: uid}
has, err := sess.Where("deleted IS NOT NULL").Get(&dashboard)
if err != nil {
return err
} else if !has {
return dashboards.ErrDashboardNotFound
}
queryResult = &dashboard
return nil
})
return queryResult, err
}
func (d *dashboardStore) RestoreDashboard(ctx context.Context, orgID int64, dashboardUID string, folder *folder.Folder) error {
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
// nolint:staticcheck
_, err := sess.Exec("UPDATE dashboard SET deleted=NULL, folder_id = ?, folder_uid=? WHERE org_id=? AND uid=?", folder.ID, folder.UID, orgID, dashboardUID)
return err
})
}
func (d *dashboardStore) SoftDeleteDashboard(ctx context.Context, orgID int64, dashboardUID string) error {
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Exec("UPDATE dashboard SET deleted=? WHERE org_id=? AND uid=?", time.Now(), orgID, dashboardUID)
return err
})
}
func (d *dashboardStore) SoftDeleteDashboardsInFolders(ctx context.Context, orgID int64, folderUids []string) error {
if len(folderUids) == 0 {
return nil
}
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
s := strings.Builder{}
s.WriteString("UPDATE dashboard SET deleted=? WHERE ")
s.WriteString(fmt.Sprintf("folder_uid IN (%s)", strings.Repeat("?,", len(folderUids)-1)+"?"))
s.WriteString(" AND org_id = ? AND is_folder = ?")
sql := s.String()
args := make([]any, 0, 3)
args = append(args, sql, time.Now())
for _, folderUID := range folderUids {
args = append(args, folderUID)
}
args = append(args, orgID, d.store.GetDialect().BooleanStr(false))
_, err := sess.Exec(args...)
return err
})
}
func (d *dashboardStore) DeleteDashboard(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
@@ -545,10 +605,13 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand,
}
if dashboard.IsFolder {
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
// if this is a soft delete, we need to skip children deletion.
if !d.features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) && !cmd.SkipSoftDeletedDashboards {
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ? AND deleted IS NULL")
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
return err
if err := d.deleteChildrenDashboardAssociations(sess, &dashboard); err != nil {
return err
}
}
// remove all access control permission with folder scope
@@ -686,7 +749,7 @@ func (d *dashboardStore) GetDashboard(ctx context.Context, query *dashboards.Get
metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc()
}
has, err := sess.MustCols(mustCols...).Nullable("folder_uid").Get(&dashboard)
has, err := sess.Where("deleted IS NULL").MustCols(mustCols...).Nullable("folder_uid").Get(&dashboard)
if err != nil {
return err
} else if !has {
@@ -726,17 +789,20 @@ func (d *dashboardStore) GetDashboards(ctx context.Context, query *dashboards.Ge
if len(query.DashboardIDs) == 0 && len(query.DashboardUIDs) == 0 {
return star.ErrCommandValidationFailed
}
var session *xorm.Session
// remove soft deleted dashboards from the response
sess.Where("deleted IS NULL")
if len(query.DashboardIDs) > 0 {
session = sess.In("id", query.DashboardIDs)
sess.In("id", query.DashboardIDs)
} else {
session = sess.In("uid", query.DashboardUIDs)
sess.In("uid", query.DashboardUIDs)
}
if query.OrgID > 0 {
session = sess.Where("org_id = ?", query.OrgID)
sess.Where("org_id = ?", query.OrgID)
}
err := session.Find(&dashboards)
err := sess.Find(&dashboards)
return err
})
if err != nil {
@@ -802,6 +868,8 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
})
}
filters = append(filters, searchstore.DeletedFilter{Deleted: query.IsDeleted})
var res []dashboards.DashboardSearchProjection
sb := &searchstore.Builder{Dialect: d.store.GetDialect(), Filters: filters, Features: d.features}
@@ -871,7 +939,7 @@ func (d *dashboardStore) CountDashboardsInFolders(
args = append(args, folderUID)
}
}
s.WriteString(" AND org_id = ? AND is_folder = ?")
s.WriteString(" AND org_id = ? AND is_folder = ? AND deleted IS NULL")
args = append(args, req.OrgID, d.store.GetDialect().BooleanStr(false))
sql := s.String()
_, err := sess.SQL(sql, args...).Get(&count)
@@ -919,6 +987,18 @@ func (d *dashboardStore) GetAllDashboards(ctx context.Context) ([]*dashboards.Da
return dashboards, nil
}
func (d *dashboardStore) GetSoftDeletedExpiredDashboards(ctx context.Context, duration time.Duration) ([]*dashboards.Dashboard, error) {
var dashboards = make([]*dashboards.Dashboard, 0)
err := d.store.WithDbSession(ctx, func(sess *db.Session) error {
err := sess.Where("deleted IS NOT NULL AND deleted < ?", time.Now().Add(-duration)).Find(&dashboards)
return err
})
if err != nil {
return nil, err
}
return dashboards, nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
@@ -525,12 +525,7 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
_ = insertTestDashboard(t, dashboardStore, "delete me 1", 1, folder.ID, folder.UID, false, "delete this 1")
_ = insertTestDashboard(t, dashboardStore, "delete me 2", 1, folder.ID, folder.UID, false, "delete this 2")
err := dashboardStore.DeleteDashboardsInFolders(
context.Background(),
&dashboards.DeleteDashboardsInFolderRequest{
FolderUIDs: []string{folder.UID},
OrgID: 1,
})
err := dashboardStore.SoftDeleteDashboardsInFolders(context.Background(), folder.OrgID, []string{folder.UID})
require.NoError(t, err)
count, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{folder.UID}, OrgID: 1})
@@ -539,6 +534,127 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
})
}
func TestIntegrationGetSoftDeletedDashboard(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
var sqlStore *sqlstore.SQLStore
var cfg *setting.Cfg
var savedFolder, savedDash *dashboards.Dashboard
var dashboardStore dashboards.Store
setup := func() {
sqlStore, cfg = db.InitTestDBWithCfg(t)
quotaService := quotatest.New(false, nil)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err)
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp")
savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.ID, savedFolder.UID, false, "prod")
}
t.Run("Should soft delete a dashboard", func(t *testing.T) {
setup()
// Confirm there are 2 dashboards in the folder
amount, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
assert.Equal(t, int64(2), amount)
// Soft delete the dashboard
err = dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
// There is only 1 dashboard in the folder after soft delete
amount, err = dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
assert.Equal(t, int64(1), amount)
var dash *dashboards.Dashboard
// Get the soft deleted dashboard should be empty
dash, _ = dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID})
assert.Error(t, dashboards.ErrDashboardNotFound)
assert.Nil(t, dash)
// Get the soft deleted dashboard
dash, err = dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
assert.Equal(t, savedDash.ID, dash.ID)
assert.Equal(t, savedDash.UID, dash.UID)
assert.Equal(t, savedDash.Title, dash.Title)
})
t.Run("Should not fail when trying to soft delete a soft deleted dashboard", func(t *testing.T) {
setup()
// Soft delete the dashboard
err := dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
// Soft delete the dashboard
err = dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
// Get the soft deleted dashboard
dash, err := dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
assert.Equal(t, savedDash.ID, dash.ID)
assert.Equal(t, savedDash.UID, dash.UID)
assert.Equal(t, savedDash.Title, dash.Title)
})
t.Run("Should restore a dashboard", func(t *testing.T) {
setup()
// Confirm there are 2 dashboards in the folder
amount, err := dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
assert.Equal(t, int64(2), amount)
// Soft delete the dashboard
err = dashboardStore.SoftDeleteDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
// There is only 1 dashboard in the folder after soft delete
amount, err = dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
assert.Equal(t, int64(1), amount)
// Get the soft deleted dashboard
dash, err := dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
require.NoError(t, err)
assert.Equal(t, savedDash.ID, dash.ID)
assert.Equal(t, savedDash.UID, dash.UID)
assert.Equal(t, savedDash.Title, dash.Title)
// Restore deleted dashboard
// nolint:staticcheck
err = dashboardStore.RestoreDashboard(context.Background(), savedDash.OrgID, savedDash.UID, &folder.Folder{ID: savedDash.FolderID, UID: savedDash.FolderUID})
require.NoError(t, err)
// Restore increases the amount of dashboards in the folder
amount, err = dashboardStore.CountDashboardsInFolders(context.Background(), &dashboards.CountDashboardsInFolderRequest{FolderUIDs: []string{savedFolder.UID}, OrgID: 1})
require.NoError(t, err)
assert.Equal(t, int64(2), amount)
// Get the soft deleted dashboard should be empty
dash, err = dashboardStore.GetSoftDeletedDashboard(context.Background(), savedDash.OrgID, savedDash.UID)
assert.Error(t, err)
assert.Nil(t, dash)
// Get the restored dashboard
dash, err = dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{UID: savedDash.UID, OrgID: savedDash.OrgID})
require.NoError(t, err)
assert.Equal(t, savedDash.ID, dash.ID)
assert.Equal(t, savedDash.UID, dash.UID)
assert.Equal(t, savedDash.Title, dash.Title)
// nolint:staticcheck
assert.Equal(t, savedDash.FolderID, dash.FolderID)
assert.Equal(t, savedDash.FolderUID, dash.FolderUID)
})
}
func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")