mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* In migration, create one label per channel This PR changes how routing is done by the legacy alerting migration. Previously, we created a single label on each alert rule that contained an array of contact point names. Ex: __contact__="slack legacy testing","slack legacy testing2" This label was then routed against a series of regex-matching policies with continue=true. Ex: __contacts__ =~ .*"slack legacy testing".* In the case of many contact points, this array could quickly become difficult to manage and difficult to grok at-a-glance. This PR replaces the single __contact__ label with multiple __legacy_c_{contactname}__ labels and simple equality-matching policies. These channel-specific policies are nested in a single route under the top-level route which matches against __legacy_use_channels__ = true for ease of organization. This should improve the experience for users wanting to keep the default migrated routing strategy but who also want to modify which contact points an alert sends to.
1434 lines
56 KiB
Go
1434 lines
56 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/prometheus/alertmanager/config"
|
|
"github.com/prometheus/alertmanager/pkg/labels"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"xorm.io/xorm"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/services/alerting/models"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
migrationStore "github.com/grafana/grafana/pkg/services/ngalert/migration/store"
|
|
ngModels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
|
)
|
|
|
|
// TestServiceStart tests the wrapper method that decides when to run the migration based on migration status and settings.
|
|
func TestServiceStart(t *testing.T) {
|
|
tc := []struct {
|
|
name string
|
|
config *setting.Cfg
|
|
starting migrationStore.AlertingType
|
|
expectedErr bool
|
|
expected migrationStore.AlertingType
|
|
}{
|
|
{
|
|
name: "when unified alerting enabled and migration not already run, then run migration",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(true),
|
|
},
|
|
},
|
|
starting: migrationStore.Legacy,
|
|
expected: migrationStore.UnifiedAlerting,
|
|
},
|
|
{
|
|
name: "when unified alerting disabled, migration is already run and CleanUpgrade is enabled, then revert migration",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(false),
|
|
Upgrade: setting.UnifiedAlertingUpgradeSettings{
|
|
CleanUpgrade: true,
|
|
},
|
|
},
|
|
},
|
|
starting: migrationStore.UnifiedAlerting,
|
|
expected: migrationStore.Legacy,
|
|
},
|
|
{
|
|
name: "when unified alerting disabled, migration is already run and CleanUpgrade is disabled, then the migration status should set to false",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(false),
|
|
Upgrade: setting.UnifiedAlertingUpgradeSettings{
|
|
CleanUpgrade: false,
|
|
},
|
|
},
|
|
},
|
|
starting: migrationStore.UnifiedAlerting,
|
|
expected: migrationStore.Legacy,
|
|
},
|
|
{
|
|
name: "when unified alerting enabled and migration is already run, then do nothing",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(true),
|
|
},
|
|
},
|
|
starting: migrationStore.UnifiedAlerting,
|
|
expected: migrationStore.UnifiedAlerting,
|
|
},
|
|
{
|
|
name: "when unified alerting disabled and migration is not already run, then do nothing",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(false),
|
|
},
|
|
},
|
|
starting: migrationStore.Legacy,
|
|
expected: migrationStore.Legacy,
|
|
},
|
|
{
|
|
name: "when unified alerting disabled, migration is already run and force migration is enabled, then revert migration",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(false),
|
|
},
|
|
ForceMigration: true,
|
|
},
|
|
starting: migrationStore.UnifiedAlerting,
|
|
expected: migrationStore.Legacy,
|
|
},
|
|
{
|
|
name: "when unified alerting disabled, migration is already run and force migration is disabled, then the migration status should set to false",
|
|
config: &setting.Cfg{
|
|
UnifiedAlerting: setting.UnifiedAlertingSettings{
|
|
Enabled: pointer(false),
|
|
},
|
|
ForceMigration: false,
|
|
},
|
|
starting: migrationStore.UnifiedAlerting,
|
|
expected: migrationStore.Legacy,
|
|
},
|
|
}
|
|
|
|
sqlStore := db.InitTestDB(t)
|
|
for _, tt := range tc {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
service := NewTestMigrationService(t, sqlStore, tt.config)
|
|
|
|
require.NoError(t, service.migrationStore.SetCurrentAlertingType(ctx, tt.starting))
|
|
|
|
err := service.Run(ctx)
|
|
if tt.expectedErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
aType, err := service.migrationStore.GetCurrentAlertingType(ctx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expected, aType)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAMConfigMigration tests the execution of the migration specifically for migrations of channels and routes.
|
|
func TestAMConfigMigration(t *testing.T) {
|
|
sqlStore := db.InitTestDB(t)
|
|
x := sqlStore.GetEngine()
|
|
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
|
tc := []struct {
|
|
name string
|
|
legacyChannels []*models.AlertNotification
|
|
alerts []*models.Alert
|
|
|
|
expected map[int64]*apimodels.PostableUserConfig
|
|
expErrors []string
|
|
}{
|
|
{
|
|
name: "general multi-org, multi-alert, multi-channel migration",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier4", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier5", "slack", slackSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier6", "opsgenie", opsgenieSettings, true), // default
|
|
},
|
|
alerts: []*models.Alert{
|
|
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1"}),
|
|
createAlert(t, 1, 1, 2, "alert2", []string{"notifier2", "notifier3"}),
|
|
createAlert(t, 1, 2, 3, "alert3", []string{"notifier3"}),
|
|
createAlert(t, 2, 3, 1, "alert4", []string{"notifier4"}),
|
|
createAlert(t, 2, 3, 2, "alert5", []string{"notifier4", "notifier5"}),
|
|
createAlert(t, 2, 4, 3, "alert6", []string{}),
|
|
},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
{Receiver: "notifier3", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier3"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
|
{Receiver: config.Receiver{Name: "notifier3"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier3", Type: "opsgenie"}}}},
|
|
},
|
|
},
|
|
},
|
|
int64(2): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier6", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
{Receiver: "notifier4", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier4"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
{Receiver: "notifier5", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier5"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier6"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier6", Type: "opsgenie"}}}},
|
|
{Receiver: config.Receiver{Name: "notifier4"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier4", Type: "email"}}}},
|
|
{Receiver: config.Receiver{Name: "notifier5"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier5", Type: "slack"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when no default channel, create empty autogen-contact-point-default",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
},
|
|
alerts: []*models.Alert{},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when multiple default channels, they all have catch-all matchers",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, true),
|
|
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, true),
|
|
},
|
|
alerts: []*models.Alert{},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchRegexp, Name: model.AlertNameLabel, Value: ".+"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when alerts share channels, only create one receiver per legacy channel",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
|
},
|
|
alerts: []*models.Alert{
|
|
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1"}),
|
|
createAlert(t, 1, 1, 1, "alert2", []string{"notifier1", "notifier2"}),
|
|
},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
{Receiver: "notifier2", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier2"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
{Receiver: config.Receiver{Name: "notifier2"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier2", Type: "slack"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when channel not linked to any alerts, still create a receiver for it",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
},
|
|
alerts: []*models.Alert{},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when unsupported channels, do not migrate them",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "hipchat", "", false),
|
|
createAlertNotification(t, int64(1), "notifier3", "sensu", "", false),
|
|
},
|
|
alerts: []*models.Alert{},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "when unsupported channel linked to alert, do not migrate only that channel",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "sensu", "", false),
|
|
},
|
|
alerts: []*models.Alert{
|
|
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1", "notifier2"}),
|
|
},
|
|
expected: map[int64]*apimodels.PostableUserConfig{
|
|
int64(1): {
|
|
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
|
|
Config: apimodels.Config{Route: &apimodels.Route{
|
|
Receiver: "autogen-contact-point-default",
|
|
GroupByStr: []string{ngModels.FolderTitleLabel, model.AlertNameLabel},
|
|
Routes: []*apimodels.Route{
|
|
{
|
|
ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: ngModels.MigratedUseLegacyChannelsLabel, Value: "true"}},
|
|
Continue: true,
|
|
Routes: []*apimodels.Route{
|
|
{Receiver: "notifier1", ObjectMatchers: apimodels.ObjectMatchers{{Type: labels.MatchEqual, Name: contactLabel("notifier1"), Value: "true"}}, Routes: nil, Continue: true, RepeatInterval: durationPointer(DisabledRepeatInterval)},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
Receivers: []*apimodels.PostableApiReceiver{
|
|
{Receiver: config.Receiver{Name: "autogen-contact-point-default"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{}},
|
|
{Receiver: config.Receiver{Name: "notifier1"}, PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{{Name: "notifier1", Type: "email"}}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "failed channel migration fails upgrade",
|
|
legacyChannels: []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "slack", brokenSettings, false),
|
|
},
|
|
alerts: []*models.Alert{
|
|
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1"}),
|
|
},
|
|
expErrors: []string{"channel 'notifier2'"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tc {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer teardown(t, x, service)
|
|
dashes := []*dashboards.Dashboard{
|
|
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
|
createDashboard(t, 2, 1, "dash2-1", 5, nil),
|
|
createDashboard(t, 3, 2, "dash3-2", 6, nil),
|
|
createDashboard(t, 4, 2, "dash4-2", 6, nil),
|
|
}
|
|
folders := []*dashboards.Dashboard{
|
|
createFolder(t, 5, 1, "folder5-1"),
|
|
createFolder(t, 6, 2, "folder6-2"),
|
|
}
|
|
setupLegacyAlertsTables(t, x, tt.legacyChannels, tt.alerts, folders, dashes)
|
|
|
|
err := service.Run(context.Background())
|
|
if len(tt.expErrors) > 0 {
|
|
for _, expErr := range tt.expErrors {
|
|
require.ErrorContains(t, err, expErr)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
for orgId := range tt.expected {
|
|
amConfig := getAlertmanagerConfig(t, x, orgId)
|
|
|
|
// Order of nested GrafanaManagedReceivers is not guaranteed.
|
|
cOpt := []cmp.Option{
|
|
cmpopts.IgnoreUnexported(apimodels.PostableApiReceiver{}),
|
|
cmpopts.IgnoreFields(apimodels.PostableGrafanaReceiver{}, "UID", "Settings", "SecureSettings"),
|
|
cmpopts.SortSlices(func(a, b *apimodels.PostableGrafanaReceiver) bool { return a.Name < b.Name }),
|
|
cmpopts.SortSlices(func(a, b *apimodels.PostableApiReceiver) bool { return a.Name < b.Name }),
|
|
}
|
|
if !cmp.Equal(tt.expected[orgId].AlertmanagerConfig.Receivers, amConfig.AlertmanagerConfig.Receivers, cOpt...) {
|
|
t.Errorf("Unexpected Receivers: %v", cmp.Diff(tt.expected[orgId].AlertmanagerConfig.Receivers, amConfig.AlertmanagerConfig.Receivers, cOpt...))
|
|
}
|
|
|
|
// Order of routes is not guaranteed.
|
|
cOpt = []cmp.Option{
|
|
cmpopts.SortSlices(func(a, b *apimodels.Route) bool {
|
|
if a.Receiver != b.Receiver {
|
|
return a.Receiver < b.Receiver
|
|
}
|
|
return a.ObjectMatchers[0].Value < b.ObjectMatchers[0].Value
|
|
}),
|
|
cmpopts.IgnoreUnexported(apimodels.Route{}, labels.Matcher{}),
|
|
cmpopts.IgnoreFields(apimodels.Route{}, "GroupBy", "GroupByAll"),
|
|
}
|
|
if !cmp.Equal(tt.expected[orgId].AlertmanagerConfig.Route, amConfig.AlertmanagerConfig.Route, cOpt...) {
|
|
t.Errorf("Unexpected Route: %v", cmp.Diff(tt.expected[orgId].AlertmanagerConfig.Route, amConfig.AlertmanagerConfig.Route, cOpt...))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDashAlertMigration tests the execution of the migration specifically for alert rules.
|
|
func TestDashAlertMigration(t *testing.T) {
|
|
withDefaults := func(lbls map[string]string) map[string]string {
|
|
lbls[ngModels.MigratedUseLegacyChannelsLabel] = "true"
|
|
return lbls
|
|
}
|
|
|
|
t.Run("when DashAlertMigration create ContactLabel on migrated AlertRules", func(t *testing.T) {
|
|
sqlStore := db.InitTestDB(t)
|
|
x := sqlStore.GetEngine()
|
|
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
|
legacyChannels := []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier4", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier5", "slack", slackSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier6", "opsgenie", opsgenieSettings, true), // default
|
|
}
|
|
alerts := []*models.Alert{
|
|
createAlert(t, 1, 1, 1, "alert1", []string{"notifier1"}),
|
|
createAlert(t, 1, 1, 2, "alert2", []string{"notifier2", "notifier3"}),
|
|
createAlert(t, 1, 2, 3, "alert3", []string{"notifier3"}),
|
|
createAlert(t, 2, 3, 1, "alert4", []string{"notifier4"}),
|
|
createAlert(t, 2, 3, 2, "alert5", []string{"notifier4", "notifier5"}),
|
|
createAlert(t, 2, 4, 3, "alert6", []string{}),
|
|
}
|
|
expected := map[int64]map[string]*ngModels.AlertRule{
|
|
int64(1): {
|
|
"alert1": {Labels: withDefaults(map[string]string{contactLabel("notifier1"): "true"})},
|
|
"alert2": {Labels: withDefaults(map[string]string{contactLabel("notifier2"): "true", contactLabel("notifier3"): "true"})},
|
|
"alert3": {Labels: withDefaults(map[string]string{contactLabel("notifier3"): "true"})},
|
|
},
|
|
int64(2): {
|
|
// Don't include default channels.
|
|
"alert4": {Labels: withDefaults(map[string]string{contactLabel("notifier4"): "true"})},
|
|
"alert5": {Labels: withDefaults(map[string]string{contactLabel("notifier4"): "true", contactLabel("notifier5"): "true"})},
|
|
"alert6": {Labels: withDefaults(map[string]string{})},
|
|
},
|
|
}
|
|
dashes := []*dashboards.Dashboard{
|
|
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
|
createDashboard(t, 2, 1, "dash2-1", 5, nil),
|
|
createDashboard(t, 3, 2, "dash3-2", 6, nil),
|
|
createDashboard(t, 4, 2, "dash4-2", 6, nil),
|
|
}
|
|
folders := []*dashboards.Dashboard{
|
|
createFolder(t, 5, 1, "folder5-1"),
|
|
createFolder(t, 6, 2, "folder6-2"),
|
|
}
|
|
setupLegacyAlertsTables(t, x, legacyChannels, alerts, folders, dashes)
|
|
err := service.Run(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
for orgId := range expected {
|
|
rules := getAlertRules(t, x, orgId)
|
|
expectedRulesMap := expected[orgId]
|
|
require.Len(t, rules, len(expectedRulesMap))
|
|
for _, r := range rules {
|
|
delete(r.Labels, "rule_uid") // Not checking this here.
|
|
exp := expectedRulesMap[r.Title].Labels
|
|
require.Lenf(t, r.Labels, len(exp), "rule doesn't have correct number of labels: %s", r.Title)
|
|
for l := range r.Labels {
|
|
require.Equal(t, exp[l], r.Labels[l])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("when folder is missing put alert in General folder", func(t *testing.T) {
|
|
sqlStore := db.InitTestDB(t)
|
|
x := sqlStore.GetEngine()
|
|
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
|
o := createOrg(t, 1)
|
|
folder1 := createFolder(t, 1, o.ID, "folder-1")
|
|
dash1 := createDashboard(t, 3, o.ID, "dash1", folder1.ID, nil)
|
|
dash2 := createDashboard(t, 4, o.ID, "dash2", 22, nil) // missing folder
|
|
|
|
a1 := createAlert(t, int(o.ID), int(dash1.ID), 1, "alert-1", []string{})
|
|
a2 := createAlert(t, int(o.ID), int(dash2.ID), 1, "alert-2", []string{})
|
|
|
|
_, err := x.Insert(o, folder1, dash1, dash2, a1, a2)
|
|
require.NoError(t, err)
|
|
|
|
err = service.Run(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
rules := getAlertRules(t, x, o.ID)
|
|
require.Len(t, rules, 2)
|
|
|
|
var generalFolder dashboards.Dashboard
|
|
_, err = x.Table(&dashboards.Dashboard{}).Where("title = ? AND org_id = ?", generalAlertingFolderTitle, o.ID).Get(&generalFolder)
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, generalFolder)
|
|
|
|
for _, rule := range rules {
|
|
var expectedFolder dashboards.Dashboard
|
|
if rule.Title == a1.Name {
|
|
expectedFolder = *folder1
|
|
} else {
|
|
expectedFolder = generalFolder
|
|
}
|
|
require.Equal(t, expectedFolder.UID, rule.NamespaceUID)
|
|
}
|
|
})
|
|
|
|
t.Run("when alert notification settings contain different combinations of id and uid", func(t *testing.T) {
|
|
sqlStore := db.InitTestDB(t)
|
|
x := sqlStore.GetEngine()
|
|
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
|
legacyChannels := []*models.AlertNotification{
|
|
createAlertNotification(t, int64(1), "notifier1", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier2", "slack", slackSettings, false),
|
|
createAlertNotification(t, int64(1), "notifier3", "opsgenie", opsgenieSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier4", "email", emailSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier5", "slack", slackSettings, false),
|
|
createAlertNotification(t, int64(2), "notifier6", "opsgenie", opsgenieSettings, true), // default
|
|
}
|
|
alerts := []*models.Alert{
|
|
createAlert(t, 1, 1, 1, "alert1", nil),
|
|
createAlert(t, 1, 1, 2, "alert2", nil),
|
|
createAlert(t, 1, 2, 3, "alert3", nil),
|
|
createAlert(t, 2, 3, 1, "alert4", nil),
|
|
createAlert(t, 2, 3, 2, "alert5", nil),
|
|
createAlert(t, 2, 4, 3, "alert6", nil),
|
|
}
|
|
alerts[0].Settings.Set("notifications", []notificationKey{{UID: "notifier1"}})
|
|
alerts[1].Settings.Set("notifications", []notificationKey{{ID: 2}, {UID: "notifier3"}})
|
|
alerts[2].Settings.Set("notifications", []notificationKey{{ID: 3, UID: "notifier4"}}) // This shouldn't happen, but if it does we choose the ID.
|
|
alerts[3].Settings.Set("notifications", []notificationKey{{ID: -99}}) // Unknown ID
|
|
alerts[4].Settings.Set("notifications", []notificationKey{{UID: "unknown"}}) // Unknown UID
|
|
alerts[5].Settings.Set("notifications", []notificationKey{{ID: -99}, {UID: "unknown"}, {UID: "notifier4"}, {ID: 5}}) // Mixed unknown and known.
|
|
|
|
expected := map[int64]map[string]*ngModels.AlertRule{
|
|
int64(1): {
|
|
"alert1": {Labels: withDefaults(map[string]string{contactLabel("notifier1"): "true"})},
|
|
"alert2": {Labels: withDefaults(map[string]string{contactLabel("notifier2"): "true", contactLabel("notifier3"): "true"})},
|
|
"alert3": {Labels: withDefaults(map[string]string{contactLabel("notifier3"): "true"})},
|
|
},
|
|
int64(2): {
|
|
// Don't include default channels.
|
|
"alert4": {Labels: withDefaults(map[string]string{})},
|
|
"alert5": {Labels: withDefaults(map[string]string{})},
|
|
"alert6": {Labels: withDefaults(map[string]string{contactLabel("notifier4"): "true", contactLabel("notifier5"): "true"})},
|
|
},
|
|
}
|
|
dashes := []*dashboards.Dashboard{
|
|
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
|
createDashboard(t, 2, 1, "dash2-1", 5, nil),
|
|
createDashboard(t, 3, 2, "dash3-2", 6, nil),
|
|
createDashboard(t, 4, 2, "dash4-2", 6, nil),
|
|
}
|
|
folders := []*dashboards.Dashboard{
|
|
createFolder(t, 5, 1, "folder5-1"),
|
|
createFolder(t, 6, 2, "folder6-2"),
|
|
}
|
|
setupLegacyAlertsTables(t, x, legacyChannels, alerts, folders, dashes)
|
|
err := service.Run(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
for orgId := range expected {
|
|
rules := getAlertRules(t, x, orgId)
|
|
expectedRulesMap := expected[orgId]
|
|
require.Len(t, rules, len(expectedRulesMap))
|
|
for _, r := range rules {
|
|
delete(r.Labels, "rule_uid") // Not checking this here.
|
|
exp := expectedRulesMap[r.Title].Labels
|
|
require.Lenf(t, r.Labels, len(exp), "rule doesn't have correct number of labels: %s", r.Title)
|
|
for l := range r.Labels {
|
|
require.Equal(t, exp[l], r.Labels[l])
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestDashAlertQueryMigration tests the execution of the migration specifically for alert rule queries.
|
|
func TestDashAlertQueryMigration(t *testing.T) {
|
|
sqlStore := db.InitTestDB(t)
|
|
x := sqlStore.GetEngine()
|
|
service := NewTestMigrationService(t, sqlStore, &setting.Cfg{})
|
|
|
|
newQueryModel := `{"datasource":{"type":"prometheus","uid":"gdev-prometheus"},"expr":"up{job=\"fake-data-gen\"}","instant":false,"interval":"%s","intervalMs":%d,"maxDataPoints":1500,"refId":"%s"}`
|
|
createAlertQueryWithModel := func(refId string, ds string, from string, to string, model string) ngModels.AlertQuery {
|
|
rel, _ := getRelativeDuration(from, to)
|
|
return ngModels.AlertQuery{
|
|
RefID: refId,
|
|
RelativeTimeRange: ngModels.RelativeTimeRange{From: rel.From, To: rel.To},
|
|
DatasourceUID: ds,
|
|
Model: []byte(model),
|
|
}
|
|
}
|
|
|
|
createAlertQuery := func(refId string, ds string, from string, to string) ngModels.AlertQuery {
|
|
dur, _ := calculateInterval(legacydata.NewDataTimeRange(from, to), simplejson.New(), nil)
|
|
return createAlertQueryWithModel(refId, ds, from, to, fmt.Sprintf(newQueryModel, "", dur.Milliseconds(), refId))
|
|
}
|
|
|
|
createClassicConditionQuery := func(refId string, conditions []classicCondition) ngModels.AlertQuery {
|
|
exprModel := struct {
|
|
Type string `json:"type"`
|
|
RefID string `json:"refId"`
|
|
Conditions []classicCondition `json:"conditions"`
|
|
}{
|
|
"classic_conditions",
|
|
refId,
|
|
conditions,
|
|
}
|
|
exprModelJSON, _ := json.Marshal(&exprModel)
|
|
|
|
q := ngModels.AlertQuery{
|
|
RefID: refId,
|
|
DatasourceUID: expressionDatasourceUID,
|
|
Model: exprModelJSON,
|
|
}
|
|
// IntervalMS and MaxDataPoints are created PreSave by AlertQuery. They don't appear to be necessary for expressions,
|
|
// but run PreSave here to match the expected model.
|
|
_ = q.PreSave()
|
|
return q
|
|
}
|
|
|
|
cond := func(refId string, reducer string, evalType string, thresh float64) classicCondition {
|
|
return classicCondition{
|
|
Evaluator: evaluator{Params: []float64{thresh}, Type: evalType},
|
|
Operator: struct {
|
|
Type string `json:"type"`
|
|
}{Type: "and"},
|
|
Query: struct {
|
|
Params []string `json:"params"`
|
|
}{Params: []string{refId}},
|
|
Reducer: struct {
|
|
Type string `json:"type"`
|
|
}{Type: reducer},
|
|
}
|
|
}
|
|
|
|
genAlert := func(mutators ...ngModels.AlertRuleMutator) *ngModels.AlertRule {
|
|
rule := &ngModels.AlertRule{
|
|
ID: 1,
|
|
OrgID: 1,
|
|
Title: "alert1",
|
|
Condition: "B",
|
|
Data: []ngModels.AlertQuery{},
|
|
IntervalSeconds: 60,
|
|
Version: 1,
|
|
NamespaceUID: "folder5-1",
|
|
DashboardUID: pointer("dash1-1"),
|
|
PanelID: pointer(int64(1)),
|
|
RuleGroup: "dash1-1",
|
|
RuleGroupIndex: 1,
|
|
NoDataState: ngModels.NoData,
|
|
ExecErrState: ngModels.AlertingErrState,
|
|
For: 60 * time.Second,
|
|
Annotations: map[string]string{
|
|
"message": "message",
|
|
},
|
|
Labels: map[string]string{ngModels.MigratedUseLegacyChannelsLabel: "true"},
|
|
IsPaused: false,
|
|
}
|
|
|
|
for _, mutator := range mutators {
|
|
mutator(rule)
|
|
}
|
|
|
|
rule.RuleGroup = fmt.Sprintf("%s - 1m", *rule.DashboardUID)
|
|
|
|
rule.Annotations["__dashboardUid__"] = *rule.DashboardUID
|
|
rule.Annotations["__panelId__"] = strconv.FormatInt(*rule.PanelID, 10)
|
|
return rule
|
|
}
|
|
|
|
type testcase struct {
|
|
name string
|
|
alerts []*models.Alert
|
|
|
|
expectedFolder *dashboards.Dashboard
|
|
expected map[int64][]*ngModels.AlertRule
|
|
expErrors []string
|
|
}
|
|
|
|
tc := []testcase{
|
|
{
|
|
name: "simple query and condition",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 1, "5m", "now")}),
|
|
createAlertWithCond(t, 2, 3, 1, "alert1", nil,
|
|
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 3, "5m", "now")}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "max", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
int64(2): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.OrgID = 2
|
|
rule.DashboardUID = pointer("dash3-2")
|
|
rule.NamespaceUID = "folder6-2"
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds3-2", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "max", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple conditions",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
|
createCondition("B", "max", "gt", 43, 2, "3m", "now"),
|
|
createCondition("C", "min", "lt", 20, 2, "3m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Condition = "D"
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("B", "ds2-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("C", "ds2-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("D", []classicCondition{
|
|
cond("A", "avg", "gt", 42),
|
|
cond("B", "max", "gt", 43),
|
|
cond("C", "min", "lt", 20),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple conditions on same query with same timerange should not create multiple queries",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
|
createCondition("A", "avg", "gt", 20, 1, "5m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Condition = "B"
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "max", "gt", 42),
|
|
cond("A", "avg", "gt", 20),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple conditions on same query with different timeranges should create multiple queries",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
|
createCondition("A", "avg", "gt", 20, 1, "3m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Condition = "C"
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "3m", "now")) // Ordered by time range.
|
|
rule.Data = append(rule.Data, createAlertQuery("B", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("C", []classicCondition{
|
|
cond("B", "max", "gt", 42),
|
|
cond("A", "avg", "gt", 20),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple conditions custom refIds",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("Q1", "avg", "gt", 42, 1, "5m", "now"),
|
|
createCondition("Q2", "max", "gt", 43, 2, "3m", "now"),
|
|
createCondition("Q3", "min", "lt", 20, 2, "3m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Condition = "A"
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("A", []classicCondition{
|
|
cond("Q1", "avg", "gt", 42),
|
|
cond("Q2", "max", "gt", 43),
|
|
cond("Q3", "min", "lt", 20),
|
|
}))
|
|
rule.Data = append(rule.Data, createAlertQuery("Q1", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("Q2", "ds2-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("Q3", "ds2-1", "3m", "now"))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple conditions out of order refIds, queries should be sorted by refId and conditions should be in original order",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("B", "avg", "gt", 42, 1, "5m", "now"),
|
|
createCondition("C", "max", "gt", 43, 2, "3m", "now"),
|
|
createCondition("A", "min", "lt", 20, 2, "3m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Condition = "D"
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds2-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("B", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("C", "ds2-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("D", []classicCondition{
|
|
cond("B", "avg", "gt", 42),
|
|
cond("C", "max", "gt", 43),
|
|
cond("A", "min", "lt", 20),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple conditions out of order with duplicate refIds",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("C", "avg", "gt", 42, 1, "5m", "now"),
|
|
createCondition("C", "max", "gt", 43, 1, "3m", "now"),
|
|
createCondition("B", "min", "lt", 20, 2, "5m", "now"),
|
|
createCondition("B", "min", "lt", 21, 2, "3m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Condition = "E"
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds2-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("B", "ds2-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("C", "ds1-1", "3m", "now"))
|
|
rule.Data = append(rule.Data, createAlertQuery("D", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("E", []classicCondition{
|
|
cond("D", "avg", "gt", 42),
|
|
cond("C", "max", "gt", 43),
|
|
cond("B", "min", "lt", 20),
|
|
cond("A", "min", "lt", 21),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alerts with unknown datasource id migrates with empty datasource uid",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{createCondition("A", "max", "gt", 42, 123, "5m", "now")}), // Unknown datasource id.
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "", "5m", "now")) // Empty datasource UID.
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "max", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alerts with unknown dashboard do not migrate",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 22, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {},
|
|
},
|
|
},
|
|
{
|
|
name: "alerts with unknown org do not migrate",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 22, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(22): {},
|
|
},
|
|
},
|
|
{
|
|
name: "alerts in general folder migrate to existing general alerting",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 8, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
createCondition("A", "avg", "gt", 42, 1, "5m", "now"),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.NamespaceUID = "General Alerting"
|
|
rule.DashboardUID = pointer("dash-in-general-1")
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds1-1", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "avg", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "alerts in general folder migrate to newly created general alerting if one doesn't exist",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 2, 9, 1, "alert1", nil, // Org 2 doesn't have general alerting folder.
|
|
[]dashAlertCondition{
|
|
createCondition("A", "avg", "gt", 42, 3, "5m", "now"),
|
|
}),
|
|
},
|
|
expectedFolder: &dashboards.Dashboard{
|
|
OrgID: 2,
|
|
Title: "General Alerting",
|
|
FolderID: 0, // nolint:staticcheck
|
|
Slug: "general-alerting",
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(2): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.OrgID = 2
|
|
rule.DashboardUID = pointer("dash-in-general-2")
|
|
rule.Data = append(rule.Data, createAlertQuery("A", "ds3-2", "5m", "now"))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "avg", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "failed alert migration fails upgrade",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{{}}),
|
|
},
|
|
expErrors: []string{"migrate alert 'alert1'"},
|
|
},
|
|
{
|
|
name: "simple query with interval, calculates intervalMs using it as min interval",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
withQueryModel(
|
|
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
|
fmt.Sprintf(queryModel, "A", "1s"),
|
|
),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Data = append(rule.Data, createAlertQueryWithModel("A", "ds1-1", "5m", "now", fmt.Sprintf(newQueryModel, "1s", 1000, "A")))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "max", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "simple query with interval as variable, calculates intervalMs using default as min interval",
|
|
alerts: []*models.Alert{
|
|
createAlertWithCond(t, 1, 1, 1, "alert1", nil,
|
|
[]dashAlertCondition{
|
|
withQueryModel(
|
|
createCondition("A", "max", "gt", 42, 1, "5m", "now"),
|
|
fmt.Sprintf(queryModel, "A", "$min_interval"),
|
|
),
|
|
}),
|
|
},
|
|
expected: map[int64][]*ngModels.AlertRule{
|
|
int64(1): {
|
|
genAlert(func(rule *ngModels.AlertRule) {
|
|
rule.Data = append(rule.Data, createAlertQueryWithModel("A", "ds1-1", "5m", "now", fmt.Sprintf(newQueryModel, "$min_interval", 1000, "A")))
|
|
rule.Data = append(rule.Data, createClassicConditionQuery("B", []classicCondition{
|
|
cond("A", "max", "gt", 42),
|
|
}))
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tc {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
defer teardown(t, x, service)
|
|
dashes := []*dashboards.Dashboard{
|
|
createDashboard(t, 1, 1, "dash1-1", 5, nil),
|
|
createDashboard(t, 2, 1, "dash2-1", 5, nil),
|
|
createDashboard(t, 3, 2, "dash3-2", 6, nil),
|
|
createDashboard(t, 4, 2, "dash4-2", 6, nil),
|
|
createDashboard(t, 8, 1, "dash-in-general-1", 0, nil),
|
|
createDashboard(t, 9, 2, "dash-in-general-2", 0, nil),
|
|
createDashboard(t, 10, 1, "dash-with-acl-1", 5, func(d *dashboards.Dashboard) {
|
|
d.Title = "Dashboard With ACL 1"
|
|
d.HasACL = true
|
|
}),
|
|
}
|
|
folders := []*dashboards.Dashboard{
|
|
createFolder(t, 5, 1, "folder5-1"),
|
|
createFolder(t, 6, 2, "folder6-2"),
|
|
createFolder(t, 7, 1, "General Alerting"),
|
|
}
|
|
setupLegacyAlertsTables(t, x, nil, tt.alerts, folders, dashes)
|
|
|
|
err := service.Run(context.Background())
|
|
if len(tt.expErrors) > 0 {
|
|
for _, expErr := range tt.expErrors {
|
|
require.ErrorContains(t, err, expErr)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
for orgId, expected := range tt.expected {
|
|
rules := getAlertRules(t, x, orgId)
|
|
|
|
for _, r := range rules {
|
|
// Remove generated fields.
|
|
require.NotEqual(t, r.Labels["rule_uid"], "")
|
|
delete(r.Labels, "rule_uid")
|
|
require.NotEqual(t, r.Annotations[ngModels.MigratedAlertIdAnnotation], "")
|
|
delete(r.Annotations, ngModels.MigratedAlertIdAnnotation)
|
|
|
|
// If folder is created, we check if separately
|
|
if tt.expectedFolder != nil {
|
|
folder := getDashboard(t, x, orgId, r.NamespaceUID)
|
|
require.Equal(t, tt.expectedFolder.Title, folder.Title)
|
|
require.Equal(t, tt.expectedFolder.OrgID, folder.OrgID)
|
|
// nolint:staticcheck
|
|
require.Equal(t, tt.expectedFolder.FolderID, folder.FolderID)
|
|
}
|
|
}
|
|
|
|
cOpt := []cmp.Option{
|
|
cmpopts.SortSlices(func(a, b *ngModels.AlertRule) bool {
|
|
return a.ID < b.ID
|
|
}),
|
|
cmpopts.IgnoreUnexported(ngModels.AlertRule{}, ngModels.AlertQuery{}),
|
|
cmpopts.IgnoreFields(ngModels.AlertRule{}, "Updated", "UID", "ID"),
|
|
}
|
|
if tt.expectedFolder != nil {
|
|
cOpt = append(cOpt, cmpopts.IgnoreFields(ngModels.AlertRule{}, "NamespaceUID"))
|
|
}
|
|
if !cmp.Equal(expected, rules, cOpt...) {
|
|
t.Errorf("Unexpected Rule: %v", cmp.Diff(expected, rules, cOpt...))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const (
|
|
emailSettings = `{"addresses": "test"}`
|
|
slackSettings = `{"recipient": "test", "token": "test"}`
|
|
opsgenieSettings = `{"apiKey": "test"}`
|
|
brokenSettings = `[{"unknown": 1.5}]`
|
|
)
|
|
|
|
var (
|
|
now = time.Now()
|
|
)
|
|
|
|
// createAlertNotificationWithReminder creates a legacy alert notification channel for inserting into the test database.
|
|
func createAlertNotificationWithReminder(t *testing.T, orgId int64, uid string, channelType string, settings string, defaultChannel bool, sendReminder bool, frequency time.Duration) *models.AlertNotification {
|
|
t.Helper()
|
|
settingsJson := simplejson.New()
|
|
if settings != "" {
|
|
s, err := simplejson.NewJson([]byte(settings))
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal alert notification json: %v", err)
|
|
}
|
|
settingsJson = s
|
|
}
|
|
|
|
return &models.AlertNotification{
|
|
OrgID: orgId,
|
|
UID: uid,
|
|
Name: uid, // Same as uid to make testing easier.
|
|
Type: channelType,
|
|
DisableResolveMessage: false,
|
|
IsDefault: defaultChannel,
|
|
Settings: settingsJson,
|
|
SecureSettings: make(map[string][]byte),
|
|
Created: now,
|
|
Updated: now,
|
|
SendReminder: sendReminder,
|
|
Frequency: frequency,
|
|
}
|
|
}
|
|
|
|
// createAlertNotification creates a legacy alert notification channel for inserting into the test database.
|
|
func createAlertNotification(t *testing.T, orgId int64, uid string, channelType string, settings string, defaultChannel bool) *models.AlertNotification {
|
|
return createAlertNotificationWithReminder(t, orgId, uid, channelType, settings, defaultChannel, false, time.Duration(0))
|
|
}
|
|
|
|
func withQueryModel(base dashAlertCondition, model string) dashAlertCondition {
|
|
base.Query.Model = []byte(model)
|
|
return base
|
|
}
|
|
|
|
var queryModel = `{"datasource":{"type":"prometheus","uid":"gdev-prometheus"},"expr":"up{job=\"fake-data-gen\"}","instant":false,"refId":"%s","interval":"%s"}`
|
|
|
|
func createCondition(refId string, reducer string, evalType string, thresh float64, datasourceId int64, from string, to string) dashAlertCondition {
|
|
return dashAlertCondition{
|
|
Evaluator: evaluator{
|
|
Params: []float64{thresh},
|
|
Type: evalType,
|
|
},
|
|
Operator: struct {
|
|
Type string `json:"type"`
|
|
}{
|
|
Type: "and",
|
|
},
|
|
Query: struct {
|
|
Params []string `json:"params"`
|
|
DatasourceID int64 `json:"datasourceId"`
|
|
Model json.RawMessage
|
|
}{
|
|
Params: []string{refId, from, to},
|
|
DatasourceID: datasourceId,
|
|
Model: []byte(fmt.Sprintf(queryModel, refId, "")),
|
|
},
|
|
Reducer: struct {
|
|
Type string `json:"type"`
|
|
}{
|
|
Type: reducer,
|
|
},
|
|
}
|
|
}
|
|
|
|
// createAlert creates a legacy alert rule for inserting into the test database.
|
|
func createAlert(t *testing.T, orgId int, dashboardId int, panelsId int, name string, notifierUids []string) *models.Alert {
|
|
return createAlertWithCond(t, orgId, dashboardId, panelsId, name, notifierUids, []dashAlertCondition{})
|
|
}
|
|
|
|
// createAlert creates a legacy alert rule for inserting into the test database.
|
|
func createAlertWithCond(t *testing.T, orgId int, dashboardId int, panelsId int, name string, notifierUids []string, cond []dashAlertCondition) *models.Alert {
|
|
t.Helper()
|
|
|
|
var settings = simplejson.New()
|
|
if len(notifierUids) != 0 {
|
|
notifiers := make([]any, 0)
|
|
for _, n := range notifierUids {
|
|
notifiers = append(notifiers, notificationKey{UID: n})
|
|
}
|
|
|
|
settings.Set("notifications", notifiers)
|
|
}
|
|
settings.Set("conditions", cond)
|
|
|
|
return &models.Alert{
|
|
OrgID: int64(orgId),
|
|
DashboardID: int64(dashboardId),
|
|
PanelID: int64(panelsId),
|
|
Name: name,
|
|
Message: "message",
|
|
Frequency: int64(60),
|
|
For: 60 * time.Second,
|
|
State: models.AlertStateOK,
|
|
Settings: settings,
|
|
NewStateDate: now,
|
|
Created: now,
|
|
Updated: now,
|
|
}
|
|
}
|
|
|
|
// createDashboard creates a folder for inserting into the test database.
|
|
func createFolder(t *testing.T, id int64, orgId int64, uid string) *dashboards.Dashboard {
|
|
f := createDashboard(t, id, orgId, uid, 0, nil)
|
|
f.IsFolder = true
|
|
return f
|
|
}
|
|
|
|
// createDashboard creates a dashboard for inserting into the test database.
|
|
func createDashboard(t *testing.T, id int64, orgId int64, uid string, folderId int64, mut func(*dashboards.Dashboard)) *dashboards.Dashboard {
|
|
t.Helper()
|
|
d := &dashboards.Dashboard{
|
|
ID: id,
|
|
OrgID: orgId,
|
|
UID: uid,
|
|
Created: now,
|
|
Updated: now,
|
|
Title: uid, // Not tested, needed to satisfy constraint.
|
|
FolderID: folderId, // nolint:staticcheck
|
|
Data: simplejson.New(),
|
|
Version: 1,
|
|
}
|
|
if mut != nil {
|
|
mut(d)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// createDatasource creates a datasource for inserting into the test database.
|
|
func createDatasource(t *testing.T, id int64, orgId int64, uid string) *datasources.DataSource {
|
|
t.Helper()
|
|
return &datasources.DataSource{
|
|
ID: id,
|
|
OrgID: orgId,
|
|
UID: uid,
|
|
Created: now,
|
|
Updated: now,
|
|
Name: uid, // Not tested, needed to satisfy constraint.
|
|
}
|
|
}
|
|
|
|
func createOrg(t *testing.T, id int64) *org.Org {
|
|
t.Helper()
|
|
return &org.Org{
|
|
ID: id,
|
|
Version: 1,
|
|
Name: fmt.Sprintf("org_%d", id),
|
|
Created: time.Now(),
|
|
Updated: time.Now(),
|
|
}
|
|
}
|
|
|
|
// teardown cleans the input tables between test cases.
|
|
func teardown(t *testing.T, x *xorm.Engine, service *migrationService) {
|
|
_, err := x.Exec("DELETE from org")
|
|
require.NoError(t, err)
|
|
_, err = x.Exec("DELETE from alert")
|
|
require.NoError(t, err)
|
|
_, err = x.Exec("DELETE from alert_notification")
|
|
require.NoError(t, err)
|
|
_, err = x.Exec("DELETE from dashboard")
|
|
require.NoError(t, err)
|
|
_, err = x.Exec("DELETE from data_source")
|
|
require.NoError(t, err)
|
|
err = service.migrationStore.RevertAllOrgs(context.Background())
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// setupLegacyAlertsTables inserts data into the legacy alerting tables that is needed for testing the
|
|
func setupLegacyAlertsTables(t *testing.T, x *xorm.Engine, legacyChannels []*models.AlertNotification, alerts []*models.Alert, folders []*dashboards.Dashboard, dashes []*dashboards.Dashboard) {
|
|
t.Helper()
|
|
|
|
orgs := []org.Org{
|
|
*createOrg(t, 1),
|
|
*createOrg(t, 2),
|
|
}
|
|
|
|
// Setup folders.
|
|
if len(folders) > 0 {
|
|
_, err := x.Insert(folders)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Setup dashboards.
|
|
if len(dashes) > 0 {
|
|
_, err := x.Insert(dashes)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Setup data_sources.
|
|
dataSources := []datasources.DataSource{
|
|
*createDatasource(t, 1, 1, "ds1-1"),
|
|
*createDatasource(t, 2, 1, "ds2-1"),
|
|
*createDatasource(t, 3, 2, "ds3-2"),
|
|
*createDatasource(t, 4, 2, "ds4-2"),
|
|
}
|
|
|
|
_, errOrgs := x.Insert(orgs)
|
|
require.NoError(t, errOrgs)
|
|
|
|
_, errDataSourcess := x.Insert(dataSources)
|
|
require.NoError(t, errDataSourcess)
|
|
|
|
if len(legacyChannels) > 0 {
|
|
_, channelErr := x.Insert(legacyChannels)
|
|
require.NoError(t, channelErr)
|
|
}
|
|
|
|
if len(alerts) > 0 {
|
|
_, alertErr := x.Insert(alerts)
|
|
require.NoError(t, alertErr)
|
|
}
|
|
}
|
|
|
|
// getAlertmanagerConfig retreives the Alertmanager Config from the database for a given orgId.
|
|
func getAlertmanagerConfig(t *testing.T, x *xorm.Engine, orgId int64) *apimodels.PostableUserConfig {
|
|
amConfig := ""
|
|
_, err := x.Table("alert_configuration").Where("org_id = ?", orgId).Cols("alertmanager_configuration").Get(&amConfig)
|
|
require.NoError(t, err)
|
|
|
|
config := apimodels.PostableUserConfig{}
|
|
err = json.Unmarshal([]byte(amConfig), &config)
|
|
require.NoError(t, err)
|
|
return &config
|
|
}
|
|
|
|
// getAlertmanagerConfig retreives the Alertmanager Config from the database for a given orgId.
|
|
func getAlertRules(t *testing.T, x *xorm.Engine, orgId int64) []*ngModels.AlertRule {
|
|
rules := make([]*ngModels.AlertRule, 0)
|
|
err := x.Table("alert_rule").Where("org_id = ?", orgId).Find(&rules)
|
|
require.NoError(t, err)
|
|
|
|
return rules
|
|
}
|
|
|
|
// getDashboard retrieves a dashboard from the database for a given org, uid.
|
|
func getDashboard(t *testing.T, x *xorm.Engine, orgId int64, uid string) *dashboards.Dashboard {
|
|
dashes := make([]*dashboards.Dashboard, 0)
|
|
err := x.Table("dashboard").Where("org_id = ? AND uid = ?", orgId, uid).Find(&dashes)
|
|
require.NoError(t, err)
|
|
if len(dashes) > 1 {
|
|
t.Error("Expected only one dashboard to be returned")
|
|
}
|
|
if len(dashes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return dashes[0]
|
|
}
|
|
|
|
func pointer[T any](b T) *T {
|
|
return &b
|
|
}
|