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>
This commit is contained in:
co-authored by
gotjosh
parent
4a9fdb8b76
commit
04d5dcb7c8
@@ -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 {
|
||||||
// Default route and receiver.
|
for orgID := range allChannels {
|
||||||
recv, route, err := m.makeReceiverAndRoute("default_route", nil, defaultChannels, allChannels)
|
if _, ok := amConfigsPerOrg[orgID]; !ok {
|
||||||
if err != nil {
|
amConfigsPerOrg[orgID] = &PostableUserConfig{
|
||||||
return err
|
AlertmanagerConfig: PostableApiAlertingConfig{
|
||||||
}
|
Receivers: make([]*PostableApiReceiver, 0),
|
||||||
|
Route: &Route{
|
||||||
|
Routes: make([]*Route, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default route and receiver.
|
||||||
|
recv, route, err := m.makeReceiverAndRoute("default_route", orgID, nil, defaultChannels[orgID], allChannels[orgID])
|
||||||
|
if err != nil {
|
||||||
|
// if one fails it will fail the migration
|
||||||
|
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 {
|
||||||
|
route.Matchers = nil // Don't need matchers for root route.
|
||||||
|
amConfigsPerOrg[orgID].AlertmanagerConfig.Route = route
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if route != nil {
|
|
||||||
route.Matchers = nil // Don't need matchers for root route.
|
|
||||||
amConfig.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,17 +75,84 @@ 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.
|
||||||
sess *xorm.Session
|
sess *xorm.Session
|
||||||
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.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *migration) SQL(dialect migrator.Dialect) string {
|
func (m *migration) SQL(dialect migrator.Dialect) string {
|
||||||
@@ -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,26 +217,11 @@ 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 {
|
if err != nil {
|
||||||
exists, err := m.sess.Where("id=?", dash.FolderId).Get(&folder)
|
return MigrationError{
|
||||||
if err != nil {
|
Err: err,
|
||||||
return MigrationError{
|
AlertId: da.Id,
|
||||||
Err: fmt.Errorf("failed to get folder %d: %w", dash.FolderId, err),
|
|
||||||
AlertId: da.Id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return MigrationError{
|
|
||||||
Err: fmt.Errorf("folder with id %v not found", dash.FolderId),
|
|
||||||
AlertId: da.Id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !folder.IsFolder {
|
|
||||||
return MigrationError{
|
|
||||||
Err: fmt.Errorf("id %v is a dashboard not a folder", dash.FolderId),
|
|
||||||
AlertId: da.Id,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,8 +278,12 @@ 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 {
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(mg.Dialect.DriverName(), migrator.Postgres) {
|
if strings.HasPrefix(mg.Dialect.DriverName(), migrator.Postgres) {
|
||||||
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a separate receiver for all the unmigrated channels.
|
for orgID, amConfig := range amConfigPerOrg {
|
||||||
err = m.addUnmigratedChannels(&amConfig, allChannels, defaultChannels)
|
// Create a separate receiver for all the unmigrated channels.
|
||||||
if err != nil {
|
err = m.addUnmigratedChannels(orgID, amConfig, allChannelsPerOrg[orgID], defaultChannelsPerOrg[orgID])
|
||||||
return err
|
if err != nil {
|
||||||
}
|
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
|
||||||
@@ -297,13 +362,15 @@ 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 _, err := sess.Insert(&record); err != nil {
|
if !m.SkipMigrationLog() {
|
||||||
return err
|
if _, err := sess.Insert(&record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
record.Success = true
|
record.Success = true
|
||||||
_, err = sess.Insert(&record)
|
if !m.SkipMigrationLog() {
|
||||||
|
_, 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)
|
func createOrg(t *testing.T, store *sqlstore.SQLStore, name string, userID int64) int64 {
|
||||||
return err
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user