grafana/pkg/services/ngalert/migration/store/database.go
Matthew Jacobson a6d928e50e
Alerting: Prevent cleanup of non-empty folders on migration revert (#76439)
Prevent cleanup of non-empty folders on revert
2023-10-12 18:40:51 -04:00

443 lines
16 KiB
Go

package store
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"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/services/accesscontrol"
legacyalerting "github.com/grafana/grafana/pkg/services/alerting"
legacymodels "github.com/grafana/grafana/pkg/services/alerting/models"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/folder"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
migmodels "github.com/grafana/grafana/pkg/services/ngalert/migration/models"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
// Store is the database abstraction for migration persistence.
type Store interface {
InsertAlertRules(ctx context.Context, rules ...models.AlertRule) error
SaveAlertmanagerConfiguration(ctx context.Context, orgID int64, amConfig *apimodels.PostableUserConfig) error
GetAllOrgs(ctx context.Context) ([]*org.OrgDTO, error)
GetDatasource(ctx context.Context, datasourceID int64, user identity.Requester) (*datasources.DataSource, error)
GetNotificationChannels(ctx context.Context, orgID int64) ([]*legacymodels.AlertNotification, error)
GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*DashAlert, int, error)
GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
GetFolderPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error)
SetDashboardPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error)
SetFolderPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error)
MapActions(permission accesscontrol.ResourcePermission) string
GetDashboard(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error)
GetFolder(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error)
CreateFolder(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error)
IsMigrated(ctx context.Context) (bool, error)
SetMigrated(ctx context.Context, migrated bool) error
GetOrgMigrationState(ctx context.Context, orgID int64) (*migmodels.OrgMigrationState, error)
SetOrgMigrationState(ctx context.Context, orgID int64, summary *migmodels.OrgMigrationState) error
RevertAllOrgs(ctx context.Context) error
CaseInsensitive() bool
}
type migrationStore struct {
store db.DB
cfg *setting.Cfg
log log.Logger
kv kvstore.KVStore
alertingStore *store.DBstore
dashboardService dashboards.DashboardService
folderService folder.Service
dataSourceCache datasources.CacheService
folderPermissions accesscontrol.FolderPermissionsService
dashboardPermissions accesscontrol.DashboardPermissionsService
orgService org.Service
legacyAlertNotificationService *legacyalerting.AlertNotificationService
}
// MigrationStore implements the Store interface.
var _ Store = (*migrationStore)(nil)
func ProvideMigrationStore(
cfg *setting.Cfg,
sqlStore db.DB,
kv kvstore.KVStore,
alertingStore *store.DBstore,
dashboardService dashboards.DashboardService,
folderService folder.Service,
dataSourceCache datasources.CacheService,
folderPermissions accesscontrol.FolderPermissionsService,
dashboardPermissions accesscontrol.DashboardPermissionsService,
orgService org.Service,
legacyAlertNotificationService *legacyalerting.AlertNotificationService,
) (Store, error) {
return &migrationStore{
log: log.New("ngalert.migration-store"),
cfg: cfg,
store: sqlStore,
kv: kv,
alertingStore: alertingStore,
dashboardService: dashboardService,
folderService: folderService,
dataSourceCache: dataSourceCache,
folderPermissions: folderPermissions,
dashboardPermissions: dashboardPermissions,
orgService: orgService,
legacyAlertNotificationService: legacyAlertNotificationService,
}, nil
}
// KVNamespace is the kvstore namespace used for the migration status.
const KVNamespace = "ngalert.migration"
// migratedKey is the kvstore key used for the migration status.
const migratedKey = "migrated"
// stateKey is the kvstore key used for the OrgMigrationState.
const stateKey = "stateKey"
const anyOrg = 0
// IsMigrated returns the migration status from the kvstore.
func (ms *migrationStore) IsMigrated(ctx context.Context) (bool, error) {
kv := kvstore.WithNamespace(ms.kv, anyOrg, KVNamespace)
content, exists, err := kv.Get(ctx, migratedKey)
if err != nil {
return false, err
}
if !exists {
return false, nil
}
return strconv.ParseBool(content)
}
// SetMigrated sets the migration status in the kvstore.
func (ms *migrationStore) SetMigrated(ctx context.Context, migrated bool) error {
kv := kvstore.WithNamespace(ms.kv, anyOrg, KVNamespace)
return kv.Set(ctx, migratedKey, strconv.FormatBool(migrated))
}
// GetOrgMigrationState returns a summary of a previous migration.
func (ms *migrationStore) GetOrgMigrationState(ctx context.Context, orgID int64) (*migmodels.OrgMigrationState, error) {
kv := kvstore.WithNamespace(ms.kv, orgID, KVNamespace)
content, exists, err := kv.Get(ctx, stateKey)
if err != nil {
return nil, err
}
if !exists {
return &migmodels.OrgMigrationState{OrgID: orgID}, nil
}
var summary migmodels.OrgMigrationState
err = json.Unmarshal([]byte(content), &summary)
if err != nil {
return nil, err
}
return &summary, nil
}
// SetOrgMigrationState sets the summary of a previous migration.
func (ms *migrationStore) SetOrgMigrationState(ctx context.Context, orgID int64, summary *migmodels.OrgMigrationState) error {
kv := kvstore.WithNamespace(ms.kv, orgID, KVNamespace)
raw, err := json.Marshal(summary)
if err != nil {
return err
}
return kv.Set(ctx, stateKey, string(raw))
}
func (ms *migrationStore) InsertAlertRules(ctx context.Context, rules ...models.AlertRule) error {
if ms.store.GetDialect().DriverName() == migrator.Postgres {
// Postgresql which will automatically rollback the whole transaction on constraint violation.
// So, for postgresql, insertions will execute in a subtransaction.
err := ms.store.InTransaction(ctx, func(subCtx context.Context) error {
_, err := ms.alertingStore.InsertAlertRules(subCtx, rules)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
} else {
_, err := ms.alertingStore.InsertAlertRules(ctx, rules)
if err != nil {
return err
}
}
return nil
}
func (ms *migrationStore) SaveAlertmanagerConfiguration(ctx context.Context, orgID int64, amConfig *apimodels.PostableUserConfig) error {
rawAmConfig, err := json.Marshal(amConfig)
if err != nil {
return err
}
cmd := models.SaveAlertmanagerConfigurationCmd{
AlertmanagerConfiguration: string(rawAmConfig),
ConfigurationVersion: fmt.Sprintf("v%d", models.AlertConfigurationVersion),
Default: false,
OrgID: orgID,
LastApplied: 0,
}
return ms.alertingStore.SaveAlertmanagerConfiguration(ctx, &cmd)
}
// revertPermissions are the permissions required for the background user to revert the migration.
var revertPermissions = []accesscontrol.Permission{
{Action: dashboards.ActionFoldersDelete, Scope: dashboards.ScopeFoldersAll},
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
}
// RevertAllOrgs reverts the migration, deleting all unified alerting resources such as alert rules, alertmanager configurations, and silence files.
// In addition, it will delete all folders and permissions originally created by this migration, these are stored in the kvstore.
func (ms *migrationStore) RevertAllOrgs(ctx context.Context) error {
return ms.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
if _, err := sess.Exec("DELETE FROM alert_rule"); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_rule_version"); err != nil {
return err
}
orgs, err := ms.GetAllOrgs(ctx)
if err != nil {
return fmt.Errorf("get orgs: %w", err)
}
for _, o := range orgs {
if err := ms.DeleteMigratedFolders(ctx, o.ID); err != nil {
ms.log.Warn("Failed to delete migrated folders", "orgID", o.ID, "err", err)
continue
}
}
if _, err := sess.Exec("DELETE FROM alert_configuration"); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM ngalert_configuration"); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM alert_instance"); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM kv_store WHERE namespace = ?", notifier.KVNamespace); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM kv_store WHERE namespace = ?", KVNamespace); err != nil {
return err
}
files, err := filepath.Glob(filepath.Join(ms.cfg.DataPath, "alerting", "*", "silences"))
if err != nil {
return err
}
for _, f := range files {
if err := os.Remove(f); err != nil {
ms.log.Error("Failed to remove silence file", "file", f, "err", err)
}
}
err = ms.SetMigrated(ctx, false)
if err != nil {
return fmt.Errorf("setting migration status: %w", err)
}
return nil
})
}
// DeleteMigratedFolders deletes all folders created by the previous migration run for the given org. This includes all folder permissions.
// If the folder is not empty of all descendants the operation will fail and return an error.
func (ms *migrationStore) DeleteMigratedFolders(ctx context.Context, orgID int64) error {
summary, err := ms.GetOrgMigrationState(ctx, orgID)
if err != nil {
return err
}
return ms.DeleteFolders(ctx, orgID, summary.CreatedFolders...)
}
var ErrFolderNotDeleted = fmt.Errorf("folder not deleted")
// DeleteFolders deletes the folders from the given orgs with the given UIDs. This includes all folder permissions.
// If the folder is not empty of all descendants the operation will fail and return an error.
func (ms *migrationStore) DeleteFolders(ctx context.Context, orgID int64, uids ...string) error {
if len(uids) == 0 {
return nil
}
var errs error
usr := accesscontrol.BackgroundUser("ngalert_migration_revert", orgID, org.RoleAdmin, revertPermissions)
for _, folderUID := range uids {
// Check if folder is empty. If not, we should not delete it.
uid := folderUID
countCmd := folder.GetDescendantCountsQuery{
UID: &uid,
OrgID: orgID,
SignedInUser: usr.(*user.SignedInUser),
}
count, err := ms.folderService.GetDescendantCounts(ctx, &countCmd)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("folder %s: %w", folderUID, err))
continue
}
var descendantCounts []string
var cntErr error
for kind, cnt := range count {
if cnt > 0 {
descendantCounts = append(descendantCounts, fmt.Sprintf("%d %s", cnt, kind))
if err != nil {
cntErr = errors.Join(cntErr, err)
continue
}
}
}
if cntErr != nil {
errs = errors.Join(errs, fmt.Errorf("folder %s: %w", folderUID, cntErr))
continue
}
if len(descendantCounts) > 0 {
errs = errors.Join(errs, fmt.Errorf("folder %s contains descendants: %s", folderUID, strings.Join(descendantCounts, ", ")))
continue
}
cmd := folder.DeleteFolderCommand{
UID: uid,
OrgID: orgID,
SignedInUser: usr.(*user.SignedInUser),
}
err = ms.folderService.Delete(ctx, &cmd) // Also handles permissions and other related entities.
if err != nil {
errs = errors.Join(errs, fmt.Errorf("folder %s: %w", folderUID, err))
continue
}
}
if errs != nil {
return fmt.Errorf("%w: %w", ErrFolderNotDeleted, errs)
}
return nil
}
func (ms *migrationStore) GetDashboard(ctx context.Context, orgID int64, id int64) (*dashboards.Dashboard, error) {
return ms.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{ID: id, OrgID: orgID})
}
func (ms *migrationStore) GetAllOrgs(ctx context.Context) ([]*org.OrgDTO, error) {
orgQuery := &org.SearchOrgsQuery{}
return ms.orgService.Search(ctx, orgQuery)
}
func (ms *migrationStore) GetDatasource(ctx context.Context, datasourceID int64, user identity.Requester) (*datasources.DataSource, error) {
return ms.dataSourceCache.GetDatasource(ctx, datasourceID, user, false)
}
// GetNotificationChannels returns all channels for this org.
func (ms *migrationStore) GetNotificationChannels(ctx context.Context, orgID int64) ([]*legacymodels.AlertNotification, error) {
return ms.legacyAlertNotificationService.GetAllAlertNotifications(ctx, &legacymodels.GetAllAlertNotificationsQuery{
OrgID: orgID,
})
}
// GetOrgDashboardAlerts loads all legacy dashboard alerts for the given org mapped by dashboard id.
func (ms *migrationStore) GetOrgDashboardAlerts(ctx context.Context, orgID int64) (map[int64][]*DashAlert, int, error) {
var alerts []legacymodels.Alert
err := ms.store.WithDbSession(ctx, func(sess *db.Session) error {
return sess.SQL("select * from alert WHERE org_id = ? AND dashboard_id IN (SELECT id from dashboard)", orgID).Find(&alerts)
})
if err != nil {
return nil, 0, err
}
mappedAlerts := make(map[int64][]*DashAlert)
for i := range alerts {
alert := alerts[i]
rawSettings, err := json.Marshal(alert.Settings)
if err != nil {
return nil, 0, fmt.Errorf("get settings for alert rule ID:%d, name:'%s', orgID:%d: %w", alert.ID, alert.Name, alert.OrgID, err)
}
var parsedSettings DashAlertSettings
err = json.Unmarshal(rawSettings, &parsedSettings)
if err != nil {
return nil, 0, fmt.Errorf("parse settings for alert rule ID:%d, name:'%s', orgID:%d: %w", alert.ID, alert.Name, alert.OrgID, err)
}
mappedAlerts[alert.DashboardID] = append(mappedAlerts[alert.DashboardID], &DashAlert{
Alert: &alerts[i],
ParsedSettings: &parsedSettings,
})
}
return mappedAlerts, len(alerts), nil
}
func (ms *migrationStore) GetDashboardPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
return ms.dashboardPermissions.GetPermissions(ctx, user, resourceID)
}
func (ms *migrationStore) GetFolderPermissions(ctx context.Context, user identity.Requester, resourceID string) ([]accesscontrol.ResourcePermission, error) {
return ms.folderPermissions.GetPermissions(ctx, user, resourceID)
}
func (ms *migrationStore) GetFolder(ctx context.Context, cmd *folder.GetFolderQuery) (*folder.Folder, error) {
return ms.folderService.Get(ctx, cmd)
}
func (ms *migrationStore) CreateFolder(ctx context.Context, cmd *folder.CreateFolderCommand) (*folder.Folder, error) {
return ms.folderService.Create(ctx, cmd)
}
func (ms *migrationStore) SetDashboardPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
return ms.dashboardPermissions.SetPermissions(ctx, orgID, resourceID, commands...)
}
func (ms *migrationStore) SetFolderPermissions(ctx context.Context, orgID int64, resourceID string, commands ...accesscontrol.SetResourcePermissionCommand) ([]accesscontrol.ResourcePermission, error) {
return ms.folderPermissions.SetPermissions(ctx, orgID, resourceID, commands...)
}
func (ms *migrationStore) MapActions(permission accesscontrol.ResourcePermission) string {
return ms.dashboardPermissions.MapActions(permission)
}
func (ms *migrationStore) CaseInsensitive() bool {
return ms.store.GetDialect().SupportEngine()
}