Alerting: modify DB table, accessors and migration to restrict org access (#37414)

* Alerting: modify table and accessors to limit org access appropriately

* Update migration to create multiple Alertmanager configs

* Apply suggestions from code review

Co-authored-by: gotjosh <josue@grafana.com>

* replace mg.ClearMigrationEntry()

mg.ClearMigrationEntry() would create a new session.
This commit introduces a new migration for clearing an entry from migration log for replacing  mg.ClearMigrationEntry() so that all dashboard alert migration operations will run inside the same transaction.
It adds also `SkipMigrationLog()` in Migrator interface for skipping adding an entry in the migration_log.

Co-authored-by: gotjosh <josue@grafana.com>
pull/37788/merge
Sofia Papagiannaki 4 years ago committed by GitHub
parent 4a9fdb8b76
commit 04d5dcb7c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      pkg/services/ngalert/api/api.go
  2. 9
      pkg/services/ngalert/api/api_alertmanager.go
  3. 7
      pkg/services/ngalert/models/alertmanager.go
  4. 20
      pkg/services/ngalert/notifier/alertmanager.go
  5. 2
      pkg/services/ngalert/notifier/alertmanager_test.go
  6. 3
      pkg/services/ngalert/store/alertmanager.go
  7. 1
      pkg/services/sqlstore/migrations/migrations.go
  8. 20
      pkg/services/sqlstore/migrations/ualert/alert_rule.go
  9. 92
      pkg/services/sqlstore/migrations/ualert/channel.go
  10. 19
      pkg/services/sqlstore/migrations/ualert/permissions.go
  11. 19
      pkg/services/sqlstore/migrations/ualert/silences.go
  12. 9
      pkg/services/sqlstore/migrations/ualert/tables.go
  13. 151
      pkg/services/sqlstore/migrations/ualert/ualert.go
  14. 4
      pkg/services/sqlstore/migrator/migrations.go
  15. 14
      pkg/services/sqlstore/migrator/migrator.go
  16. 4
      pkg/services/sqlstore/migrator/types.go
  17. 63
      pkg/tests/api/alerting/api_alertmanager_configuration_test.go
  18. 92
      pkg/tests/api/alerting/api_alertmanager_test.go
  19. 6
      pkg/tests/api/alerting/api_available_channel_test.go
  20. 6
      pkg/tests/api/alerting/api_notification_channel_test.go
  21. 12
      pkg/tests/api/alerting/api_prometheus_test.go
  22. 12
      pkg/tests/api/alerting/api_ruler_test.go

@ -22,8 +22,10 @@ var timeNow = time.Now
type Alertmanager interface { type Alertmanager interface {
// Configuration // Configuration
SaveAndApplyConfig(config *apimodels.PostableUserConfig) error // temporary add orgID parameter; this will move to the Alertmanager wrapper when it will be available
SaveAndApplyDefaultConfig() error SaveAndApplyConfig(orgID int64, config *apimodels.PostableUserConfig) error
// temporary add orgID parameter; this will move to the Alertmanager wrapper when it will be available
SaveAndApplyDefaultConfig(orgID int64) error
GetStatus() apimodels.GettableStatus GetStatus() apimodels.GettableStatus
// Silences // Silences

@ -48,7 +48,7 @@ func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) respo
if !c.HasUserRole(models.ROLE_EDITOR) { if !c.HasUserRole(models.ROLE_EDITOR) {
return ErrResp(http.StatusForbidden, errors.New("permission denied"), "") return ErrResp(http.StatusForbidden, errors.New("permission denied"), "")
} }
if err := srv.am.SaveAndApplyDefaultConfig(); err != nil { if err := srv.am.SaveAndApplyDefaultConfig(c.OrgId); err != nil {
srv.log.Error("unable to save and apply default alertmanager configuration", "err", err) srv.log.Error("unable to save and apply default alertmanager configuration", "err", err)
return ErrResp(http.StatusInternalServerError, err, "failed to save and apply default Alertmanager configuration") return ErrResp(http.StatusInternalServerError, err, "failed to save and apply default Alertmanager configuration")
} }
@ -74,7 +74,8 @@ func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response
if !c.HasUserRole(models.ROLE_EDITOR) { if !c.HasUserRole(models.ROLE_EDITOR) {
return ErrResp(http.StatusForbidden, errors.New("permission denied"), "") return ErrResp(http.StatusForbidden, errors.New("permission denied"), "")
} }
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{}
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: c.OrgId}
if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil { if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil {
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
return ErrResp(http.StatusNotFound, err, "") return ErrResp(http.StatusNotFound, err, "")
@ -201,7 +202,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap
} }
// Get the last known working configuration // Get the last known working configuration
query := ngmodels.GetLatestAlertmanagerConfigurationQuery{} query := ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: c.OrgId}
if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil { if err := srv.store.GetLatestAlertmanagerConfiguration(&query); err != nil {
// If we don't have a configuration there's nothing for us to know and we should just continue saving the new one // If we don't have a configuration there's nothing for us to know and we should just continue saving the new one
if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) { if !errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
@ -255,7 +256,7 @@ func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body ap
return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration") return ErrResp(http.StatusInternalServerError, err, "failed to post process Alertmanager configuration")
} }
if err := srv.am.SaveAndApplyConfig(&body); err != nil { if err := srv.am.SaveAndApplyConfig(c.OrgId, &body); err != nil {
srv.log.Error("unable to save and apply alertmanager configuration", "err", err) srv.log.Error("unable to save and apply alertmanager configuration", "err", err)
return ErrResp(http.StatusBadRequest, err, "failed to save and apply Alertmanager configuration") return ErrResp(http.StatusBadRequest, err, "failed to save and apply Alertmanager configuration")
} }

@ -10,10 +10,12 @@ type AlertConfiguration struct {
ConfigurationVersion string ConfigurationVersion string
CreatedAt int64 `xorm:"created"` CreatedAt int64 `xorm:"created"`
Default bool Default bool
OrgID int64 `xorm:"org_id"`
} }
// GetLatestAlertmanagerConfigurationQuery is the query to get the latest alertmanager configuration. // GetLatestAlertmanagerConfigurationQuery is the query to get the latest alertmanager configuration.
type GetLatestAlertmanagerConfigurationQuery struct { type GetLatestAlertmanagerConfigurationQuery struct {
OrgID int64
Result *AlertConfiguration Result *AlertConfiguration
} }
@ -22,8 +24,5 @@ type SaveAlertmanagerConfigurationCmd struct {
AlertmanagerConfiguration string AlertmanagerConfiguration string
ConfigurationVersion string ConfigurationVersion string
Default bool Default bool
} OrgID int64
type DeleteAlertmanagerConfigurationCmd struct {
ID int64
} }

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strconv"
"sync" "sync"
"time" "time"
@ -72,6 +73,8 @@ const (
} }
} }
` `
//TODO: temporary until fix org isolation
mainOrgID = 1
) )
type Alertmanager struct { type Alertmanager struct {
@ -168,7 +171,7 @@ func (am *Alertmanager) Ready() bool {
func (am *Alertmanager) Run(ctx context.Context) error { func (am *Alertmanager) Run(ctx context.Context) error {
// Make sure dispatcher starts. We can tolerate future reload failures. // Make sure dispatcher starts. We can tolerate future reload failures.
if err := am.SyncAndApplyConfigFromDatabase(); err != nil { if err := am.SyncAndApplyConfigFromDatabase(mainOrgID); err != nil {
am.logger.Error("unable to sync configuration", "err", err) am.logger.Error("unable to sync configuration", "err", err)
} }
@ -177,7 +180,7 @@ func (am *Alertmanager) Run(ctx context.Context) error {
case <-ctx.Done(): case <-ctx.Done():
return am.StopAndWait() return am.StopAndWait()
case <-time.After(pollInterval): case <-time.After(pollInterval):
if err := am.SyncAndApplyConfigFromDatabase(); err != nil { if err := am.SyncAndApplyConfigFromDatabase(mainOrgID); err != nil {
am.logger.Error("unable to sync configuration", "err", err) am.logger.Error("unable to sync configuration", "err", err)
} }
} }
@ -203,7 +206,7 @@ func (am *Alertmanager) StopAndWait() error {
// SaveAndApplyDefaultConfig saves the default configuration the database and applies the configuration to the Alertmanager. // SaveAndApplyDefaultConfig saves the default configuration the database and applies the configuration to the Alertmanager.
// It rollbacks the save if we fail to apply the configuration. // It rollbacks the save if we fail to apply the configuration.
func (am *Alertmanager) SaveAndApplyDefaultConfig() error { func (am *Alertmanager) SaveAndApplyDefaultConfig(orgID int64) error {
am.reloadConfigMtx.Lock() am.reloadConfigMtx.Lock()
defer am.reloadConfigMtx.Unlock() defer am.reloadConfigMtx.Unlock()
@ -211,6 +214,7 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig() error {
AlertmanagerConfiguration: alertmanagerDefaultConfiguration, AlertmanagerConfiguration: alertmanagerDefaultConfiguration,
Default: true, Default: true,
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion), ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
OrgID: orgID,
} }
cfg, err := Load([]byte(alertmanagerDefaultConfiguration)) cfg, err := Load([]byte(alertmanagerDefaultConfiguration))
@ -234,7 +238,7 @@ func (am *Alertmanager) SaveAndApplyDefaultConfig() error {
// SaveAndApplyConfig saves the configuration the database and applies the configuration to the Alertmanager. // SaveAndApplyConfig saves the configuration the database and applies the configuration to the Alertmanager.
// It rollbacks the save if we fail to apply the configuration. // It rollbacks the save if we fail to apply the configuration.
func (am *Alertmanager) SaveAndApplyConfig(cfg *apimodels.PostableUserConfig) error { func (am *Alertmanager) SaveAndApplyConfig(orgID int64, cfg *apimodels.PostableUserConfig) error {
rawConfig, err := json.Marshal(&cfg) rawConfig, err := json.Marshal(&cfg)
if err != nil { if err != nil {
return fmt.Errorf("failed to serialize to the Alertmanager configuration: %w", err) return fmt.Errorf("failed to serialize to the Alertmanager configuration: %w", err)
@ -246,6 +250,7 @@ func (am *Alertmanager) SaveAndApplyConfig(cfg *apimodels.PostableUserConfig) er
cmd := &ngmodels.SaveAlertmanagerConfigurationCmd{ cmd := &ngmodels.SaveAlertmanagerConfigurationCmd{
AlertmanagerConfiguration: string(rawConfig), AlertmanagerConfiguration: string(rawConfig),
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion), ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
OrgID: orgID,
} }
err = am.Store.SaveAlertmanagerConfigurationWithCallback(cmd, func() error { err = am.Store.SaveAlertmanagerConfigurationWithCallback(cmd, func() error {
@ -264,12 +269,12 @@ func (am *Alertmanager) SaveAndApplyConfig(cfg *apimodels.PostableUserConfig) er
// SyncAndApplyConfigFromDatabase picks the latest config from database and restarts // SyncAndApplyConfigFromDatabase picks the latest config from database and restarts
// the components with the new config. // the components with the new config.
func (am *Alertmanager) SyncAndApplyConfigFromDatabase() error { func (am *Alertmanager) SyncAndApplyConfigFromDatabase(orgID int64) error {
am.reloadConfigMtx.Lock() am.reloadConfigMtx.Lock()
defer am.reloadConfigMtx.Unlock() defer am.reloadConfigMtx.Unlock()
// First, let's get the configuration we need from the database. // First, let's get the configuration we need from the database.
q := &ngmodels.GetLatestAlertmanagerConfigurationQuery{} q := &ngmodels.GetLatestAlertmanagerConfigurationQuery{OrgID: mainOrgID}
if err := am.Store.GetLatestAlertmanagerConfiguration(q); err != nil { if err := am.Store.GetLatestAlertmanagerConfiguration(q); err != nil {
// If there's no configuration in the database, let's use the default configuration. // If there's no configuration in the database, let's use the default configuration.
if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { if errors.Is(err, store.ErrNoAlertmanagerConfiguration) {
@ -279,6 +284,7 @@ func (am *Alertmanager) SyncAndApplyConfigFromDatabase() error {
AlertmanagerConfiguration: alertmanagerDefaultConfiguration, AlertmanagerConfiguration: alertmanagerDefaultConfiguration,
Default: true, Default: true,
ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion), ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion),
OrgID: orgID,
} }
if err := am.Store.SaveAlertmanagerConfiguration(savecmd); err != nil { if err := am.Store.SaveAlertmanagerConfiguration(savecmd); err != nil {
return err return err
@ -399,7 +405,7 @@ func (am *Alertmanager) applyConfig(cfg *apimodels.PostableUserConfig, rawConfig
} }
func (am *Alertmanager) WorkingDirPath() string { func (am *Alertmanager) WorkingDirPath() string {
return filepath.Join(am.Settings.DataPath, workingDir) return filepath.Join(am.Settings.DataPath, workingDir, strconv.Itoa(mainOrgID))
} }
// buildIntegrationsMap builds a map of name to the list of Grafana integration notifiers off of a list of receiver config. // buildIntegrationsMap builds a map of name to the list of Grafana integration notifiers off of a list of receiver config.

@ -54,7 +54,7 @@ func setupAMTest(t *testing.T) *Alertmanager {
func TestAlertmanager_ShouldUseDefaultConfigurationWhenNoConfiguration(t *testing.T) { func TestAlertmanager_ShouldUseDefaultConfigurationWhenNoConfiguration(t *testing.T) {
am := setupAMTest(t) am := setupAMTest(t)
require.NoError(t, am.SyncAndApplyConfigFromDatabase()) require.NoError(t, am.SyncAndApplyConfigFromDatabase(mainOrgID))
require.NotNil(t, am.config) require.NotNil(t, am.config)
} }

@ -19,7 +19,7 @@ func (st *DBstore) GetLatestAlertmanagerConfiguration(query *models.GetLatestAle
return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
c := &models.AlertConfiguration{} c := &models.AlertConfiguration{}
// The ID is already an auto incremental column, using the ID as an order should guarantee the latest. // The ID is already an auto incremental column, using the ID as an order should guarantee the latest.
ok, err := sess.Desc("id").Limit(1).Get(c) ok, err := sess.Desc("id").Where("org_id = ?", query.OrgID).Limit(1).Get(c)
if err != nil { if err != nil {
return err return err
} }
@ -48,6 +48,7 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(cmd *models.SaveAler
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration, AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
ConfigurationVersion: cmd.ConfigurationVersion, ConfigurationVersion: cmd.ConfigurationVersion,
Default: cmd.Default, Default: cmd.Default,
OrgID: cmd.OrgID,
} }
if _, err := sess.Insert(config); err != nil { if _, err := sess.Insert(config); err != nil {
return err return err

@ -41,6 +41,7 @@ func AddMigrations(mg *Migrator) {
ualert.AddTablesMigrations(mg) ualert.AddTablesMigrations(mg)
ualert.AddDashAlertMigration(mg) ualert.AddDashAlertMigration(mg)
addLibraryElementsMigrations(mg) addLibraryElementsMigrations(mg)
ualert.RerunDashAlertMigration(mg)
} }
func addMigrationLogMigrations(mg *Migrator) { func addMigrationLogMigrations(mg *Migrator) {

@ -9,14 +9,14 @@ import (
) )
type alertRule struct { type alertRule struct {
OrgId int64 OrgID int64 `xorm:"org_id"`
Title string Title string
Condition string Condition string
Data []alertQuery Data []alertQuery
IntervalSeconds int64 IntervalSeconds int64
Version int64 Version int64
Uid string UID string `xorm:"uid"`
NamespaceUid string NamespaceUID string `xorm:"namespace_uid"`
RuleGroup string RuleGroup string
NoDataState string NoDataState string
ExecErrState string ExecErrState string
@ -51,9 +51,9 @@ type alertRuleVersion struct {
func (a *alertRule) makeVersion() *alertRuleVersion { func (a *alertRule) makeVersion() *alertRuleVersion {
return &alertRuleVersion{ return &alertRuleVersion{
RuleOrgID: a.OrgId, RuleOrgID: a.OrgID,
RuleUID: a.Uid, RuleUID: a.UID,
RuleNamespaceUID: a.NamespaceUid, RuleNamespaceUID: a.NamespaceUID,
RuleGroup: a.RuleGroup, RuleGroup: a.RuleGroup,
ParentVersion: 0, ParentVersion: 0,
RestoredFrom: 0, RestoredFrom: 0,
@ -96,14 +96,14 @@ func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string
annotations["message"] = da.Message annotations["message"] = da.Message
ar := &alertRule{ ar := &alertRule{
OrgId: da.OrgId, OrgID: da.OrgId,
Title: da.Name, // TODO: Make sure all names are unique, make new name on constraint insert error. Title: da.Name, // TODO: Make sure all names are unique, make new name on constraint insert error.
Uid: util.GenerateShortUID(), UID: util.GenerateShortUID(),
Condition: cond.Condition, Condition: cond.Condition,
Data: cond.Data, Data: cond.Data,
IntervalSeconds: ruleAdjustInterval(da.Frequency), IntervalSeconds: ruleAdjustInterval(da.Frequency),
Version: 1, Version: 1,
NamespaceUid: folderUID, // Folder already created, comes from env var. NamespaceUID: folderUID, // Folder already created, comes from env var.
RuleGroup: da.Name, RuleGroup: da.Name,
For: duration(da.For), For: duration(da.For),
Updated: time.Now().UTC(), Updated: time.Now().UTC(),
@ -123,7 +123,7 @@ func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string
} }
// Label for routing and silences. // Label for routing and silences.
n, v := getLabelForRouteMatching(ar.Uid) n, v := getLabelForRouteMatching(ar.UID)
ar.Labels[n] = v ar.Labels[n] = v
if err := m.addSilence(da, ar); err != nil { if err := m.addSilence(da, ar); err != nil {

@ -17,7 +17,8 @@ import (
) )
type notificationChannel struct { type notificationChannel struct {
ID int `xorm:"id"` ID int64 `xorm:"id"`
OrgID int64 `xorm:"org_id"`
Uid string `xorm:"uid"` Uid string `xorm:"uid"`
Name string `xorm:"name"` Name string `xorm:"name"`
Type string `xorm:"type"` Type string `xorm:"type"`
@ -27,9 +28,16 @@ type notificationChannel struct {
SecureSettings securejsondata.SecureJsonData `xorm:"secure_settings"` SecureSettings securejsondata.SecureJsonData `xorm:"secure_settings"`
} }
func (m *migration) getNotificationChannelMap() (map[interface{}]*notificationChannel, []*notificationChannel, error) { // channelsPerOrg maps notification channels per organisation
type channelsPerOrg map[int64]map[interface{}]*notificationChannel
// channelMap maps notification channels per organisation
type defaultChannelsPerOrg map[int64][]*notificationChannel
func (m *migration) getNotificationChannelMap() (channelsPerOrg, defaultChannelsPerOrg, error) {
q := ` q := `
SELECT id, SELECT id,
org_id,
uid, uid,
name, name,
type, type,
@ -50,25 +58,27 @@ func (m *migration) getNotificationChannelMap() (map[interface{}]*notificationCh
return nil, nil, nil return nil, nil, nil
} }
allChannelsMap := make(map[interface{}]*notificationChannel) allChannelsMap := make(channelsPerOrg)
var defaultChannels []*notificationChannel defaultChannelsMap := make(defaultChannelsPerOrg)
for i, c := range allChannels { for i, c := range allChannels {
if _, ok := allChannelsMap[c.OrgID]; !ok { // new seen org
allChannelsMap[c.OrgID] = make(map[interface{}]*notificationChannel)
}
if c.Uid != "" { if c.Uid != "" {
allChannelsMap[c.Uid] = &allChannels[i] allChannelsMap[c.OrgID][c.Uid] = &allChannels[i]
} }
if c.ID != 0 { if c.ID != 0 {
allChannelsMap[c.ID] = &allChannels[i] allChannelsMap[c.OrgID][c.ID] = &allChannels[i]
} }
if c.IsDefault { if c.IsDefault {
// TODO: verify that there will be only 1 default channel. defaultChannelsMap[c.OrgID] = append(defaultChannelsMap[c.OrgID], &allChannels[i])
defaultChannels = append(defaultChannels, &allChannels[i])
} }
} }
return allChannelsMap, defaultChannels, nil return allChannelsMap, defaultChannelsMap, nil
} }
func (m *migration) updateReceiverAndRoute(allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel, da dashAlert, rule *alertRule, amConfig *PostableUserConfig) error { func (m *migration) updateReceiverAndRoute(allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg, da dashAlert, rule *alertRule, amConfig *PostableUserConfig) error {
// Create receiver and route for this rule. // Create receiver and route for this rule.
if allChannels == nil { if allChannels == nil {
return nil return nil
@ -82,7 +92,7 @@ func (m *migration) updateReceiverAndRoute(allChannels map[interface{}]*notifica
return nil return nil
} }
recv, route, err := m.makeReceiverAndRoute(rule.Uid, channelIDs, defaultChannels, allChannels) recv, route, err := m.makeReceiverAndRoute(rule.UID, rule.OrgID, channelIDs, defaultChannels[rule.OrgID], allChannels[rule.OrgID])
if err != nil { if err != nil {
return err return err
} }
@ -97,7 +107,7 @@ func (m *migration) updateReceiverAndRoute(allChannels map[interface{}]*notifica
return nil return nil
} }
func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface{}, defaultChannels []*notificationChannel, allChannels map[interface{}]*notificationChannel) (*PostableApiReceiver, *Route, error) { func (m *migration) makeReceiverAndRoute(ruleUid string, orgID int64, channelUids []interface{}, defaultChannels []*notificationChannel, allChannels map[interface{}]*notificationChannel) (*PostableApiReceiver, *Route, error) {
portedChannels := []*PostableGrafanaReceiver{} portedChannels := []*PostableGrafanaReceiver{}
var receiver *PostableApiReceiver var receiver *PostableApiReceiver
@ -112,7 +122,10 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
return errors.New("failed to generate UID for notification channel") return errors.New("failed to generate UID for notification channel")
} }
m.migratedChannels[c] = struct{}{} if _, ok := m.migratedChannelsPerOrg[orgID]; !ok {
m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{})
}
m.migratedChannelsPerOrg[orgID][c] = struct{}{}
settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings) settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
portedChannels = append(portedChannels, &PostableGrafanaReceiver{ portedChannels = append(portedChannels, &PostableGrafanaReceiver{
UID: uid, UID: uid,
@ -129,9 +142,10 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
// Remove obsolete notification channels. // Remove obsolete notification channels.
filteredChannelUids := make(map[interface{}]struct{}) filteredChannelUids := make(map[interface{}]struct{})
for _, uid := range channelUids { for _, uid := range channelUids {
_, ok := allChannels[uid] c, ok := allChannels[uid]
if ok { if ok {
filteredChannelUids[uid] = struct{}{} // always store the channel UID to prevent duplicates
filteredChannelUids[c.Uid] = struct{}{}
} else { } else {
m.mg.Logger.Warn("ignoring obsolete notification channel", "uid", uid) m.mg.Logger.Warn("ignoring obsolete notification channel", "uid", uid)
} }
@ -142,9 +156,10 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
if c.Uid == "" { if c.Uid == "" {
id = c.ID id = c.ID
} }
_, ok := allChannels[id] c, ok := allChannels[id]
if ok { if ok {
filteredChannelUids[id] = struct{}{} // always store the channel UID to prevent duplicates
filteredChannelUids[c.Uid] = struct{}{}
} }
} }
@ -159,7 +174,11 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
} }
var receiverName string var receiverName string
if rn, ok := m.portedChannelGroups[chanKey]; ok {
if _, ok := m.portedChannelGroupsPerOrg[orgID]; !ok {
m.portedChannelGroupsPerOrg[orgID] = make(map[string]string)
}
if rn, ok := m.portedChannelGroupsPerOrg[orgID][chanKey]; ok {
// We have ported these exact set of channels already. Re-use it. // We have ported these exact set of channels already. Re-use it.
receiverName = rn receiverName = rn
if receiverName == "autogen-contact-point-default" { if receiverName == "autogen-contact-point-default" {
@ -180,7 +199,7 @@ func (m *migration) makeReceiverAndRoute(ruleUid string, channelUids []interface
receiverName = fmt.Sprintf("autogen-contact-point-%d", m.lastReceiverID) receiverName = fmt.Sprintf("autogen-contact-point-%d", m.lastReceiverID)
} }
m.portedChannelGroups[chanKey] = receiverName m.portedChannelGroupsPerOrg[orgID][chanKey] = receiverName
receiver = &PostableApiReceiver{ receiver = &PostableApiReceiver{
Name: receiverName, Name: receiverName,
GrafanaManagedReceivers: portedChannels, GrafanaManagedReceivers: portedChannels,
@ -220,32 +239,47 @@ func makeKeyForChannelGroup(channelUids map[interface{}]struct{}) (string, error
} }
// addDefaultChannels should be called before adding any other routes. // addDefaultChannels should be called before adding any other routes.
func (m *migration) addDefaultChannels(amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error { func (m *migration) addDefaultChannels(amConfigsPerOrg amConfigsPerOrg, allChannels channelsPerOrg, defaultChannels defaultChannelsPerOrg) error {
for orgID := range allChannels {
if _, ok := amConfigsPerOrg[orgID]; !ok {
amConfigsPerOrg[orgID] = &PostableUserConfig{
AlertmanagerConfig: PostableApiAlertingConfig{
Receivers: make([]*PostableApiReceiver, 0),
Route: &Route{
Routes: make([]*Route, 0),
},
},
}
}
// Default route and receiver. // Default route and receiver.
recv, route, err := m.makeReceiverAndRoute("default_route", nil, defaultChannels, allChannels) recv, route, err := m.makeReceiverAndRoute("default_route", orgID, nil, defaultChannels[orgID], allChannels[orgID])
if err != nil { if err != nil {
// if one fails it will fail the migration
return err return err
} }
if recv != nil { if recv != nil {
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, recv) amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers = append(amConfigsPerOrg[orgID].AlertmanagerConfig.Receivers, recv)
} }
if route != nil { if route != nil {
route.Matchers = nil // Don't need matchers for root route. route.Matchers = nil // Don't need matchers for root route.
amConfig.AlertmanagerConfig.Route = route amConfigsPerOrg[orgID].AlertmanagerConfig.Route = route
}
} }
return nil return nil
} }
func (m *migration) addUnmigratedChannels(amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error { func (m *migration) addUnmigratedChannels(orgID int64, amConfigs *PostableUserConfig, allChannels map[interface{}]*notificationChannel, defaultChannels []*notificationChannel) error {
// Unmigrated channels. // Unmigrated channels.
portedChannels := []*PostableGrafanaReceiver{} portedChannels := []*PostableGrafanaReceiver{}
receiver := &PostableApiReceiver{ receiver := &PostableApiReceiver{
Name: "autogen-unlinked-channel-recv", Name: "autogen-unlinked-channel-recv",
} }
for _, c := range allChannels { for _, c := range allChannels {
_, ok := m.migratedChannels[c] if _, ok := m.migratedChannelsPerOrg[orgID]; !ok {
m.migratedChannelsPerOrg[orgID] = make(map[*notificationChannel]struct{})
}
_, ok := m.migratedChannelsPerOrg[orgID][c]
if ok { if ok {
continue continue
} }
@ -259,7 +293,7 @@ func (m *migration) addUnmigratedChannels(amConfig *PostableUserConfig, allChann
return errors.New("failed to generate UID for notification channel") return errors.New("failed to generate UID for notification channel")
} }
m.migratedChannels[c] = struct{}{} m.migratedChannelsPerOrg[orgID][c] = struct{}{}
settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings) settings, secureSettings := migrateSettingsToSecureSettings(c.Type, c.Settings, c.SecureSettings)
portedChannels = append(portedChannels, &PostableGrafanaReceiver{ portedChannels = append(portedChannels, &PostableGrafanaReceiver{
UID: uid, UID: uid,
@ -272,7 +306,7 @@ func (m *migration) addUnmigratedChannels(amConfig *PostableUserConfig, allChann
} }
receiver.GrafanaManagedReceivers = portedChannels receiver.GrafanaManagedReceivers = portedChannels
if len(portedChannels) > 0 { if len(portedChannels) > 0 {
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, receiver) amConfigs.AlertmanagerConfig.Receivers = append(amConfigs.AlertmanagerConfig.Receivers, receiver)
} }
return nil return nil
@ -361,6 +395,8 @@ type PostableUserConfig struct {
AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"` AlertmanagerConfig PostableApiAlertingConfig `yaml:"alertmanager_config" json:"alertmanager_config"`
} }
type amConfigsPerOrg = map[int64]*PostableUserConfig
func (c *PostableUserConfig) EncryptSecureSettings() error { func (c *PostableUserConfig) EncryptSecureSettings() error {
for _, r := range c.AlertmanagerConfig.Receivers { for _, r := range c.AlertmanagerConfig.Receivers {
for _, gr := range r.GrafanaManagedReceivers { for _, gr := range r.GrafanaManagedReceivers {

@ -61,6 +61,25 @@ func (m *migration) getOrCreateGeneralFolder(orgID int64) (*dashboard, error) {
return &dashboard, nil return &dashboard, nil
} }
// returns the folder of the given dashboard (if exists)
func (m *migration) getFolder(dash dashboard, da dashAlert) (dashboard, error) {
// get folder if exists
folder := dashboard{}
if dash.FolderId > 0 {
exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
if err != nil {
return folder, fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err)
}
if !exists {
return folder, fmt.Errorf("folder with id %v not found", dash.FolderId)
}
if !folder.IsFolder {
return folder, fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId)
}
}
return folder, nil
}
// based on sqlstore.saveDashboard() // based on sqlstore.saveDashboard()
// it should be called from inside a transaction // it should be called from inside a transaction
func (m *migration) createFolder(orgID int64, title string) (*dashboard, error) { func (m *migration) createFolder(orgID int64, title string) (*dashboard, error) {

@ -8,6 +8,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"time" "time"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
@ -27,7 +28,7 @@ func (m *migration) addSilence(da dashAlert, rule *alertRule) error {
return errors.New("failed to create uuid for silence") return errors.New("failed to create uuid for silence")
} }
n, v := getLabelForRouteMatching(rule.Uid) n, v := getLabelForRouteMatching(rule.UID)
s := &pb.MeshSilence{ s := &pb.MeshSilence{
Silence: &pb.Silence{ Silence: &pb.Silence{
Id: uid.String(), Id: uid.String(),
@ -50,7 +51,7 @@ func (m *migration) addSilence(da dashAlert, rule *alertRule) error {
return nil return nil
} }
func (m *migration) writeSilencesFile() error { func (m *migration) writeSilencesFile(orgID int64) error {
var buf bytes.Buffer var buf bytes.Buffer
for _, e := range m.silences { for _, e := range m.silences {
if _, err := pbutil.WriteDelimited(&buf, e); err != nil { if _, err := pbutil.WriteDelimited(&buf, e); err != nil {
@ -58,7 +59,7 @@ func (m *migration) writeSilencesFile() error {
} }
} }
f, err := openReplace(silencesFileName(m.mg)) f, err := openReplace(silencesFileNameForOrg(m.mg, orgID))
if err != nil { if err != nil {
return err return err
} }
@ -70,8 +71,12 @@ func (m *migration) writeSilencesFile() error {
return f.Close() return f.Close()
} }
func silencesFileName(mg *migrator.Migrator) string { func getSilenceFileNamesForAllOrgs(mg *migrator.Migrator) ([]string, error) {
return filepath.Join(mg.Cfg.DataPath, "alerting", "silences") return filepath.Glob(filepath.Join(mg.Cfg.DataPath, "alerting", "*", "silences"))
}
func silencesFileNameForOrg(mg *migrator.Migrator, orgID int64) string {
return filepath.Join(mg.Cfg.DataPath, "alerting", strconv.Itoa(int(orgID)), "silences")
} }
// replaceFile wraps a file that is moved to another filename on closing. // replaceFile wraps a file that is moved to another filename on closing.
@ -94,6 +99,10 @@ func (f *replaceFile) Close() error {
func openReplace(filename string) (*replaceFile, error) { func openReplace(filename string) (*replaceFile, error) {
tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63())) tmpFilename := fmt.Sprintf("%s.%x", filename, uint64(rand.Int63()))
if err := os.MkdirAll(filepath.Dir(tmpFilename), os.ModePerm); err != nil {
return nil, err
}
f, err := os.Create(tmpFilename) f, err := os.Create(tmpFilename)
if err != nil { if err != nil {
return nil, err return nil, err

@ -267,6 +267,15 @@ func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) {
mg.AddMigration("alert alert_configuration alertmanager_configuration column from TEXT to MEDIUMTEXT if mysql", migrator.NewRawSQLMigration(""). mg.AddMigration("alert alert_configuration alertmanager_configuration column from TEXT to MEDIUMTEXT if mysql", migrator.NewRawSQLMigration("").
Mysql("ALTER TABLE alert_configuration MODIFY alertmanager_configuration MEDIUMTEXT;")) Mysql("ALTER TABLE alert_configuration MODIFY alertmanager_configuration MEDIUMTEXT;"))
mg.AddMigration("add column org_id in alert_configuration", migrator.NewAddColumnMigration(alertConfiguration, &migrator.Column{
Name: "org_id", Type: migrator.DB_BigInt, Nullable: false, Default: "0",
}))
// add index on org_id
mg.AddMigration("add index in alert_configuration table on org_id column", migrator.NewAddIndexMigration(alertConfiguration, &migrator.Index{
Cols: []string{"org_id"},
}))
} }
func AddAlertAdminConfigMigrations(mg *migrator.Migrator) { func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {

@ -23,6 +23,8 @@ var migTitle = "move dashboard alerts to unified alerting"
var rmMigTitle = "remove unified alerting data" var rmMigTitle = "remove unified alerting data"
const clearMigrationEntryTitle = "clear migration entry %q"
type MigrationError struct { type MigrationError struct {
AlertId int64 AlertId int64
Err error Err error
@ -49,19 +51,23 @@ func AddDashAlertMigration(mg *migrator.Migrator) {
case ngEnabled && !migrationRun: case ngEnabled && !migrationRun:
// Remove the migration entry that removes all unified alerting data. This is so when the feature // Remove the migration entry that removes all unified alerting data. This is so when the feature
// flag is removed in future the "remove unified alerting data" migration will be run again. // flag is removed in future the "remove unified alerting data" migration will be run again.
err = mg.ClearMigrationEntry(rmMigTitle) mg.AddMigration(fmt.Sprintf(clearMigrationEntryTitle, rmMigTitle), &clearMigrationEntry{
migrationID: rmMigTitle,
})
if err != nil { if err != nil {
mg.Logger.Error("alert migration error: could not clear alert migration for removing data", "error", err) mg.Logger.Error("alert migration error: could not clear alert migration for removing data", "error", err)
} }
mg.AddMigration(migTitle, &migration{ mg.AddMigration(migTitle, &migration{
seenChannelUIDs: make(map[string]struct{}), seenChannelUIDs: make(map[string]struct{}),
migratedChannels: make(map[*notificationChannel]struct{}), migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
portedChannelGroups: make(map[string]string), portedChannelGroupsPerOrg: make(map[int64]map[string]string),
}) })
case !ngEnabled && migrationRun: case !ngEnabled && migrationRun:
// Remove the migration entry that creates unified alerting data. This is so when the feature // Remove the migration entry that creates unified alerting data. This is so when the feature
// flag is enabled in the future the migration "move dashboard alerts to unified alerting" will be run again. // flag is enabled in the future the migration "move dashboard alerts to unified alerting" will be run again.
err = mg.ClearMigrationEntry(migTitle) mg.AddMigration(fmt.Sprintf(clearMigrationEntryTitle, migTitle), &clearMigrationEntry{
migrationID: migTitle,
})
if err != nil { if err != nil {
mg.Logger.Error("alert migration error: could not clear dashboard alert migration", "error", err) mg.Logger.Error("alert migration error: could not clear dashboard alert migration", "error", err)
} }
@ -69,6 +75,73 @@ func AddDashAlertMigration(mg *migrator.Migrator) {
} }
} }
// RerunDashAlertMigration force the dashboard alert migration to run
// to make sure that the Alertmanager configurations will be created for each organisation
func RerunDashAlertMigration(mg *migrator.Migrator) {
logs, err := mg.GetMigrationLog()
if err != nil {
mg.Logger.Crit("alert migration failure: could not get migration log", "error", err)
os.Exit(1)
}
cloneMigTitle := fmt.Sprintf("clone %s", migTitle)
cloneRmMigTitle := fmt.Sprintf("clone %s", rmMigTitle)
_, migrationRun := logs[cloneMigTitle]
ngEnabled := mg.Cfg.IsNgAlertEnabled()
switch {
case ngEnabled && !migrationRun:
// Removes all unified alerting data. It is not recorded so when the feature
// flag is removed in future the "clone remove unified alerting data" migration will be run again.
mg.AddMigration(cloneRmMigTitle, &rmMigrationWithoutLogging{})
mg.AddMigration(cloneMigTitle, &migration{
seenChannelUIDs: make(map[string]struct{}),
migratedChannelsPerOrg: make(map[int64]map[*notificationChannel]struct{}),
portedChannelGroupsPerOrg: make(map[int64]map[string]string),
})
case !ngEnabled && migrationRun:
// Remove the migration entry that creates unified alerting data. This is so when the feature
// flag is enabled in the future the migration "move dashboard alerts to unified alerting" will be run again.
mg.AddMigration(fmt.Sprintf(clearMigrationEntryTitle, cloneMigTitle), &clearMigrationEntry{
migrationID: cloneMigTitle,
})
if err != nil {
mg.Logger.Error("alert migration error: could not clear clone dashboard alert migration", "error", err)
}
// Removes all unified alerting data. It is not recorded so when the feature
// flag is enabled in future the "clone remove unified alerting data" migration will be run again.
mg.AddMigration(cloneRmMigTitle, &rmMigrationWithoutLogging{})
}
}
// clearMigrationEntry removes an entry fromt the migration_log table.
// This migration is not recorded in the migration_log so that it can re-run several times.
type clearMigrationEntry struct {
migrator.MigrationBase
migrationID string
}
func (m *clearMigrationEntry) SQL(dialect migrator.Dialect) string {
return "clear migration entry code migration"
}
func (m *clearMigrationEntry) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
_, err := sess.SQL(`DELETE from migration_log where migration_id = ?`, m.migrationID).Query()
if err != nil {
return fmt.Errorf("failed to clear migration entry %v: %w", m.migrationID, err)
}
return nil
}
func (m *clearMigrationEntry) SkipMigrationLog() bool {
return true
}
type migration struct { type migration struct {
migrator.MigrationBase migrator.MigrationBase
// session and mg are attached for convenience. // session and mg are attached for convenience.
@ -76,9 +149,9 @@ type migration struct {
mg *migrator.Migrator mg *migrator.Migrator
seenChannelUIDs map[string]struct{} seenChannelUIDs map[string]struct{}
migratedChannels map[*notificationChannel]struct{} migratedChannelsPerOrg map[int64]map[*notificationChannel]struct{}
silences []*pb.MeshSilence silences []*pb.MeshSilence
portedChannelGroups map[string]string // Channel group key -> receiver name. portedChannelGroupsPerOrg map[int64]map[string]string // Org -> Channel group key -> receiver name.
lastReceiverID int // For the auto generated receivers. lastReceiverID int // For the auto generated receivers.
} }
@ -108,13 +181,13 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
} }
// allChannels: channelUID -> channelConfig // allChannels: channelUID -> channelConfig
allChannels, defaultChannels, err := m.getNotificationChannelMap() allChannelsPerOrg, defaultChannelsPerOrg, err := m.getNotificationChannelMap()
if err != nil { if err != nil {
return err return err
} }
amConfig := PostableUserConfig{} amConfigPerOrg := make(amConfigsPerOrg, len(allChannelsPerOrg))
err = m.addDefaultChannels(&amConfig, allChannels, defaultChannels) err = m.addDefaultChannels(amConfigPerOrg, allChannelsPerOrg, defaultChannelsPerOrg)
if err != nil { if err != nil {
return err return err
} }
@ -144,28 +217,13 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
} }
// get folder if exists // get folder if exists
folder := dashboard{} folder, err := m.getFolder(dash, da)
if dash.FolderId > 0 {
exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
if err != nil { if err != nil {
return MigrationError{ return MigrationError{
Err: fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err), Err: err,
AlertId: da.Id,
}
}
if !exists {
return MigrationError{
Err: fmt.Errorf("folder with id %v not found", dash.FolderId),
AlertId: da.Id, AlertId: da.Id,
} }
} }
if !folder.IsFolder {
return MigrationError{
Err: fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId),
AlertId: da.Id,
}
}
}
switch { switch {
case dash.HasAcl: case dash.HasAcl:
@ -220,9 +278,13 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
return err return err
} }
if err := m.updateReceiverAndRoute(allChannels, defaultChannels, da, rule, &amConfig); err != nil { if _, ok := amConfigPerOrg[rule.OrgID]; !ok {
m.mg.Logger.Info("no configuration found", "org", rule.OrgID)
} else {
if err := m.updateReceiverAndRoute(allChannelsPerOrg, defaultChannelsPerOrg, da, rule, amConfigPerOrg[rule.OrgID]); err != nil {
return err return err
} }
}
if strings.HasPrefix(mg.Dialect.DriverName(), migrator.Postgres) { if strings.HasPrefix(mg.Dialect.DriverName(), migrator.Postgres) {
err = mg.InTransaction(func(sess *xorm.Session) error { err = mg.InTransaction(func(sess *xorm.Session) error {
@ -234,8 +296,8 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
} }
if err != nil { if err != nil {
// TODO better error handling, if constraint // TODO better error handling, if constraint
rule.Title += fmt.Sprintf(" %v", rule.Uid) rule.Title += fmt.Sprintf(" %v", rule.UID)
rule.RuleGroup += fmt.Sprintf(" %v", rule.Uid) rule.RuleGroup += fmt.Sprintf(" %v", rule.UID)
_, err = m.sess.Insert(rule) _, err = m.sess.Insert(rule)
if err != nil { if err != nil {
@ -250,24 +312,26 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
} }
} }
for orgID, amConfig := range amConfigPerOrg {
// Create a separate receiver for all the unmigrated channels. // Create a separate receiver for all the unmigrated channels.
err = m.addUnmigratedChannels(&amConfig, allChannels, defaultChannels) err = m.addUnmigratedChannels(orgID, amConfig, allChannelsPerOrg[orgID], defaultChannelsPerOrg[orgID])
if err != nil { if err != nil {
return err return err
} }
if err := m.writeAlertmanagerConfig(&amConfig, allChannels); err != nil { if err := m.writeAlertmanagerConfig(orgID, amConfig, allChannelsPerOrg[orgID]); err != nil {
return err return err
} }
if err := m.writeSilencesFile(); err != nil { if err := m.writeSilencesFile(orgID); err != nil {
m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err) m.mg.Logger.Error("alert migration error: failed to write silence file", "err", err)
} }
}
return nil return nil
} }
func (m *migration) writeAlertmanagerConfig(amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel) error { func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserConfig, allChannels map[interface{}]*notificationChannel) error {
if len(allChannels) == 0 { if len(allChannels) == 0 {
// No channels, hence don't require Alertmanager config. // No channels, hence don't require Alertmanager config.
m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config") m.mg.Logger.Info("alert migration: no notification channel found, skipping Alertmanager config")
@ -288,6 +352,7 @@ func (m *migration) writeAlertmanagerConfig(amConfig *PostableUserConfig, allCha
// Since we are migration for a snapshot of the code, it is always going to migrate to // Since we are migration for a snapshot of the code, it is always going to migrate to
// the v1 config. // the v1 config.
ConfigurationVersion: "v1", ConfigurationVersion: "v1",
OrgID: orgID,
}) })
if err != nil { if err != nil {
return err return err
@ -298,12 +363,14 @@ func (m *migration) writeAlertmanagerConfig(amConfig *PostableUserConfig, allCha
type AlertConfiguration struct { type AlertConfiguration struct {
ID int64 `xorm:"pk autoincr 'id'"` ID int64 `xorm:"pk autoincr 'id'"`
OrgID int64 `xorm:"org_id"`
AlertmanagerConfiguration string AlertmanagerConfiguration string
ConfigurationVersion string ConfigurationVersion string
CreatedAt int64 `xorm:"created"` CreatedAt int64 `xorm:"created"`
} }
// rmMigration removes Grafana 8 alert data
type rmMigration struct { type rmMigration struct {
migrator.MigrationBase migrator.MigrationBase
} }
@ -343,9 +410,23 @@ func (m *rmMigration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
return err return err
} }
if err := os.RemoveAll(silencesFileName(mg)); err != nil { files, err := getSilenceFileNamesForAllOrgs(mg)
mg.Logger.Error("alert migration error: failed to remove silence file", "err", err) if err != nil {
return err
}
for _, f := range files {
if err := os.Remove(f); err != nil {
mg.Logger.Error("alert migration error: failed to remove silence file", "file", f, "err", err)
}
} }
return nil return nil
} }
// rmMigrationWithoutLogging is similar migration to rmMigration
// but is not recorded in the migration_log table so that it can rerun in the future
type rmMigrationWithoutLogging = rmMigration
func (m *rmMigrationWithoutLogging) SkipMigrationLog() bool {
return true
}

@ -21,6 +21,10 @@ func (m *MigrationBase) GetCondition() MigrationCondition {
return m.Condition return m.Condition
} }
func (m *MigrationBase) SkipMigrationLog() bool {
return false
}
type RawSQLMigration struct { type RawSQLMigration struct {
MigrationBase MigrationBase

@ -108,13 +108,17 @@ func (mg *Migrator) Start() error {
if err != nil { if err != nil {
mg.Logger.Error("Exec failed", "error", err, "sql", sql) mg.Logger.Error("Exec failed", "error", err, "sql", sql)
record.Error = err.Error() record.Error = err.Error()
if !m.SkipMigrationLog() {
if _, err := sess.Insert(&record); err != nil { if _, err := sess.Insert(&record); err != nil {
return err return err
} }
}
return err return err
} }
record.Success = true record.Success = true
if !m.SkipMigrationLog() {
_, err = sess.Insert(&record) _, err = sess.Insert(&record)
}
if err == nil { if err == nil {
migrationsPerformed++ migrationsPerformed++
} }
@ -171,16 +175,6 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error {
return nil return nil
} }
func (mg *Migrator) ClearMigrationEntry(id string) error {
sess := mg.x.NewSession()
defer sess.Close()
_, err := sess.SQL(`DELETE from migration_log where migration_id = ?`, id).Query()
if err != nil {
return fmt.Errorf("failed to clear migration entry %v: %w", id, err)
}
return nil
}
type dbTransactionFunc func(sess *xorm.Session) error type dbTransactionFunc func(sess *xorm.Session) error
func (mg *Migrator) InTransaction(callback dbTransactionFunc) error { func (mg *Migrator) InTransaction(callback dbTransactionFunc) error {

@ -19,6 +19,10 @@ type Migration interface {
Id() string Id() string
SetId(string) SetId(string)
GetCondition() MigrationCondition GetCondition() MigrationCondition
// SkipMigrationLog is used by dashboard alert migration to Grafana 8 Alerts
// for skipping recording it in the migration_log so that it can run several times.
// For all the other migrations it should be false.
SkipMigrationLog() bool
} }
type CodeMigration interface { type CodeMigration interface {

@ -4,8 +4,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"regexp"
"testing" "testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/tests/testinfra" "github.com/grafana/grafana/pkg/tests/testinfra"
@ -16,12 +18,34 @@ import (
func TestAlertmanagerConfigurationIsTransactional(t *testing.T) { func TestAlertmanagerConfigurationIsTransactional(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"}, EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR, DisableAnonymous: true,
}) })
store := testinfra.SetUpDatabase(t, dir) store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
alertConfigURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// create user under main organisation
userID := createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "editor",
Login: "editor",
})
// create another organisation
orgID := createOrg(t, store, "another org", userID)
// create user under different organisation
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "editor-42",
Login: "editor-42",
OrgId: orgID,
})
// editor from main organisation requests configuration
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// On a blank start with no configuration, it saves and delivers the default configuration. // On a blank start with no configuration, it saves and delivers the default configuration.
{ {
@ -66,17 +90,48 @@ func TestAlertmanagerConfigurationIsTransactional(t *testing.T) {
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body)) require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
} }
// editor42 from organisation 42 posts configuration
alertConfigURL = fmt.Sprintf("http://editor-42:editor-42@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// Post the alertmanager config.
{
mockChannel := newMockNotificationChannel(t, grafanaListedAddr)
amConfig := getAlertmanagerConfig(mockChannel.server.Addr)
postRequest(t, alertConfigURL, amConfig, http.StatusAccepted) // nolint
// Verifying that the new configuration is returned
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
b := getBody(t, resp.Body)
re := regexp.MustCompile(`"uid":"([\w|-]*)"`)
e := getExpAlertmanagerConfigFromAPI(mockChannel.server.Addr)
require.JSONEq(t, e, string(re.ReplaceAll([]byte(b), []byte(`"uid":""`))))
}
// verify that main organisation still gets the default configuration
alertConfigURL = fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
} }
func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) { func TestAlertmanagerConfigurationPersistSecrets(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"}, EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR, DisableAnonymous: true,
}) })
store := testinfra.SetUpDatabase(t, dir) store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
alertConfigURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "editor",
Login: "editor",
})
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
generatedUID := "" generatedUID := ""
// create a new configuration that has a secret // create a new configuration that has a secret

@ -39,9 +39,21 @@ func TestAMConfigAccess(t *testing.T) {
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create a users to make authenticated requests // Create a users to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer")) createUser(t, store, models.CreateUserCommand{
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor")) DefaultOrgRole: string(models.ROLE_VIEWER),
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin")) Password: "viewer",
Login: "viewer",
})
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "editor",
Login: "editor",
})
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_ADMIN),
Password: "admin",
Login: "admin",
})
type testCase struct { type testCase struct {
desc string desc string
@ -402,7 +414,11 @@ func TestAlertAndGroupsQuery(t *testing.T) {
} }
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
// invalid credentials request to get the alerts should fail // invalid credentials request to get the alerts should fail
{ {
@ -554,9 +570,21 @@ func TestRulerAccess(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create a users to make authenticated requests // Create a users to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer")) createUser(t, store, models.CreateUserCommand{
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor")) DefaultOrgRole: string(models.ROLE_VIEWER),
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin")) Password: "viewer",
Login: "viewer",
})
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "editor",
Login: "editor",
})
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_ADMIN),
Password: "admin",
Login: "admin",
})
// Now, let's test the access policies. // Now, let's test the access policies.
testCases := []struct { testCases := []struct {
@ -668,8 +696,16 @@ func TestDeleteFolderWithRules(t *testing.T) {
namespaceUID, err := createFolder(t, store, 0, "default") namespaceUID, err := createFolder(t, store, 0, "default")
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer")) createUser(t, store, models.CreateUserCommand{
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor")) DefaultOrgRole: string(models.ROLE_VIEWER),
Password: "viewer",
Login: "viewer",
})
createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "editor",
Login: "editor",
})
createRule(t, grafanaListedAddr, "default", "editor", "editor") createRule(t, grafanaListedAddr, "default", "editor", "editor")
@ -815,12 +851,14 @@ func TestAlertRuleCRUD(t *testing.T) {
store.Bus = bus.GetBus() store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
err := createUser(t, store, models.ROLE_EDITOR, "grafana", "password") createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
require.NoError(t, err) Password: "password",
Login: "grafana",
})
// Create the namespace we'll save our alerts to. // Create the namespace we'll save our alerts to.
_, err = createFolder(t, store, 0, "default") _, err := createFolder(t, store, 0, "default")
require.NoError(t, err) require.NoError(t, err)
interval, err := model.ParseDuration("1m") interval, err := model.ParseDuration("1m")
@ -1827,7 +1865,11 @@ func TestQuota(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
interval, err := model.ParseDuration("1m") interval, err := model.ParseDuration("1m")
require.NoError(t, err) require.NoError(t, err)
@ -1921,7 +1963,11 @@ func TestEval(t *testing.T) {
store.Bus = bus.GetBus() store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
// Create the namespace we'll save our alerts to. // Create the namespace we'll save our alerts to.
_, err := createFolder(t, store, 0, "default") _, err := createFolder(t, store, 0, "default")
@ -2338,16 +2384,18 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st
return string(json), m return string(json), m
} }
func createUser(t *testing.T, store *sqlstore.SQLStore, role models.RoleType, username, password string) error { func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) int64 {
t.Helper() t.Helper()
cmd := models.CreateUserCommand{ u, err := store.CreateUser(context.Background(), cmd)
Login: username, require.NoError(t, err)
Password: password, return u.Id
DefaultOrgRole: string(role),
} }
_, err := store.CreateUser(context.Background(), cmd)
return err func createOrg(t *testing.T, store *sqlstore.SQLStore, name string, userID int64) int64 {
org, err := store.CreateOrgWithMember(name, userID)
require.NoError(t, err)
return org.Id
} }
func getLongString(t *testing.T, n int) string { func getLongString(t *testing.T, n int) string {

@ -24,7 +24,11 @@ func TestAvailableChannels(t *testing.T) {
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr) alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alert-notifiers", grafanaListedAddr)
// nolint:gosec // nolint:gosec

@ -68,7 +68,11 @@ func TestNotificationChannels(t *testing.T) {
bus.AddHandlerCtx("", mockEmail.sendEmailCommandHandlerSync) bus.AddHandlerCtx("", mockEmail.sendEmailCommandHandlerSync)
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, s, models.ROLE_EDITOR, "grafana", "password")) createUser(t, s, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
{ {
// There are no notification channel config initially - so it returns the default configuration. // There are no notification channel config initially - so it returns the default configuration.

@ -34,7 +34,11 @@ func TestPrometheusRules(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
interval, err := model.ParseDuration("10s") interval, err := model.ParseDuration("10s")
require.NoError(t, err) require.NoError(t, err)
@ -270,7 +274,11 @@ func TestPrometheusRulesPermissions(t *testing.T) {
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
// Create a namespace under default organisation (orgID = 1) where we'll save some alerts. // Create a namespace under default organisation (orgID = 1) where we'll save some alerts.
_, err := createFolder(t, store, 0, "folder1") _, err := createFolder(t, store, 0, "folder1")

@ -31,7 +31,11 @@ func TestAlertRulePermissions(t *testing.T) {
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store) grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create a user to make authenticated requests // Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: "password",
Login: "grafana",
})
// Create the namespace we'll save our alerts to. // Create the namespace we'll save our alerts to.
_, err := createFolder(t, store, 0, "folder1") _, err := createFolder(t, store, 0, "folder1")
@ -320,7 +324,11 @@ func TestAlertRuleConflictingTitle(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create user // Create user
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin")) createUser(t, store, models.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_ADMIN),
Password: "admin",
Login: "admin",
})
interval, err := model.ParseDuration("1m") interval, err := model.ParseDuration("1m")
require.NoError(t, err) require.NoError(t, err)

Loading…
Cancel
Save