mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: add collision safe update function for alertmanager configurations (#46692)
* Alerting: add collision safe update function for alertmanager configurations * fix typo * use bootstrap func for tests * move hash calculation to store * remove icons lol * remove removed field
This commit is contained in:
parent
ff3c1e3144
commit
a80f04c949
@ -7,6 +7,7 @@ type AlertConfiguration struct {
|
|||||||
ID int64 `xorm:"pk autoincr 'id'"`
|
ID int64 `xorm:"pk autoincr 'id'"`
|
||||||
|
|
||||||
AlertmanagerConfiguration string
|
AlertmanagerConfiguration string
|
||||||
|
ConfigurationHash string
|
||||||
ConfigurationVersion string
|
ConfigurationVersion string
|
||||||
CreatedAt int64 `xorm:"created"`
|
CreatedAt int64 `xorm:"created"`
|
||||||
Default bool
|
Default bool
|
||||||
@ -22,6 +23,7 @@ type GetLatestAlertmanagerConfigurationQuery struct {
|
|||||||
// SaveAlertmanagerConfigurationCmd is the command to save an alertmanager configuration.
|
// SaveAlertmanagerConfigurationCmd is the command to save an alertmanager configuration.
|
||||||
type SaveAlertmanagerConfigurationCmd struct {
|
type SaveAlertmanagerConfigurationCmd struct {
|
||||||
AlertmanagerConfiguration string
|
AlertmanagerConfiguration string
|
||||||
|
FetchedConfigurationHash string
|
||||||
ConfigurationVersion string
|
ConfigurationVersion string
|
||||||
Default bool
|
Default bool
|
||||||
OrgID int64
|
OrgID int64
|
||||||
|
@ -2,6 +2,9 @@ package notifier
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@ -67,6 +70,20 @@ func (f *FakeConfigStore) SaveAlertmanagerConfigurationWithCallback(_ context.Co
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeConfigStore) UpdateAlertManagerConfiguration(cmd *models.SaveAlertmanagerConfigurationCmd) error {
|
||||||
|
if config, exists := f.configs[cmd.OrgID]; exists && config.ConfigurationHash == cmd.FetchedConfigurationHash {
|
||||||
|
f.configs[cmd.OrgID] = &models.AlertConfiguration{
|
||||||
|
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
||||||
|
OrgID: cmd.OrgID,
|
||||||
|
ConfigurationHash: fmt.Sprintf("%x", md5.Sum([]byte(cmd.AlertmanagerConfiguration))),
|
||||||
|
ConfigurationVersion: "v1",
|
||||||
|
Default: cmd.Default,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("config not found or hash not valid")
|
||||||
|
}
|
||||||
|
|
||||||
type FakeOrgStore struct {
|
type FakeOrgStore struct {
|
||||||
orgs []int64
|
orgs []int64
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@ -13,6 +14,9 @@ import (
|
|||||||
var (
|
var (
|
||||||
// ErrNoAlertmanagerConfiguration is an error for when no alertmanager configuration is found.
|
// ErrNoAlertmanagerConfiguration is an error for when no alertmanager configuration is found.
|
||||||
ErrNoAlertmanagerConfiguration = fmt.Errorf("could not find an Alertmanager configuration")
|
ErrNoAlertmanagerConfiguration = fmt.Errorf("could not find an Alertmanager configuration")
|
||||||
|
// ErrVersionLockedObjectNotFound is returned when an object is not
|
||||||
|
// found using the current hash.
|
||||||
|
ErrVersionLockedObjectNotFound = fmt.Errorf("could not find object using provided id and hash")
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetLatestAlertmanagerConfiguration returns the lastest version of the alertmanager configuration.
|
// GetLatestAlertmanagerConfiguration returns the lastest version of the alertmanager configuration.
|
||||||
@ -64,6 +68,7 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(ctx context.Context,
|
|||||||
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
config := models.AlertConfiguration{
|
config := models.AlertConfiguration{
|
||||||
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
||||||
|
ConfigurationHash: fmt.Sprintf("%x", md5.Sum([]byte(cmd.AlertmanagerConfiguration))),
|
||||||
ConfigurationVersion: cmd.ConfigurationVersion,
|
ConfigurationVersion: cmd.ConfigurationVersion,
|
||||||
Default: cmd.Default,
|
Default: cmd.Default,
|
||||||
OrgID: cmd.OrgID,
|
OrgID: cmd.OrgID,
|
||||||
@ -79,3 +84,31 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(ctx context.Context,
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (st *DBstore) UpdateAlertManagerConfiguration(cmd *models.SaveAlertmanagerConfigurationCmd) error {
|
||||||
|
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
config := models.AlertConfiguration{
|
||||||
|
AlertmanagerConfiguration: cmd.AlertmanagerConfiguration,
|
||||||
|
ConfigurationHash: fmt.Sprintf("%x", md5.Sum([]byte(cmd.AlertmanagerConfiguration))),
|
||||||
|
ConfigurationVersion: cmd.ConfigurationVersion,
|
||||||
|
Default: cmd.Default,
|
||||||
|
OrgID: cmd.OrgID,
|
||||||
|
}
|
||||||
|
rows, err := sess.Table("alert_configuration").Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM alert_configuration
|
||||||
|
WHERE
|
||||||
|
org_id = ?
|
||||||
|
AND
|
||||||
|
id = (SELECT MAX(id) FROM alert_configuration WHERE org_id = ?)
|
||||||
|
AND
|
||||||
|
configuration_hash = ?
|
||||||
|
)`,
|
||||||
|
cmd.OrgID, cmd.OrgID, cmd.FetchedConfigurationHash).Insert(config)
|
||||||
|
if rows == 0 {
|
||||||
|
return ErrVersionLockedObjectNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
84
pkg/services/ngalert/store/alertmanager_test.go
Normal file
84
pkg/services/ngalert/store/alertmanager_test.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//go:build integration
|
||||||
|
// +build integration
|
||||||
|
|
||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertManagerHash(t *testing.T) {
|
||||||
|
sqlStore := sqlstore.InitTestDB(t)
|
||||||
|
store := &DBstore{
|
||||||
|
SQLStore: sqlStore,
|
||||||
|
}
|
||||||
|
setupConfig := func(t *testing.T, config string) (string, string) {
|
||||||
|
config, configMD5 := config, fmt.Sprintf("%x", md5.Sum([]byte(config)))
|
||||||
|
err := store.SaveAlertmanagerConfiguration(context.Background(), &models.SaveAlertmanagerConfigurationCmd{
|
||||||
|
AlertmanagerConfiguration: config,
|
||||||
|
ConfigurationVersion: "v1",
|
||||||
|
Default: false,
|
||||||
|
OrgID: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return config, configMD5
|
||||||
|
}
|
||||||
|
t.Run("After saving the DB should return the right hash", func(t *testing.T) {
|
||||||
|
_, configMD5 := setupConfig(t, "my-config")
|
||||||
|
req := &models.GetLatestAlertmanagerConfigurationQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
}
|
||||||
|
err := store.GetLatestAlertmanagerConfiguration(context.Background(), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, configMD5, req.Result.ConfigurationHash)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("When passing the right hash the config should be updated", func(t *testing.T) {
|
||||||
|
_, configMD5 := setupConfig(t, "my-config")
|
||||||
|
req := &models.GetLatestAlertmanagerConfigurationQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
}
|
||||||
|
err := store.GetLatestAlertmanagerConfiguration(context.Background(), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, configMD5, req.Result.ConfigurationHash)
|
||||||
|
newConfig, newConfigMD5 := "my-config-new", fmt.Sprintf("%x", md5.Sum([]byte("my-config-new")))
|
||||||
|
err = store.UpdateAlertManagerConfiguration(&models.SaveAlertmanagerConfigurationCmd{
|
||||||
|
AlertmanagerConfiguration: newConfig,
|
||||||
|
FetchedConfigurationHash: configMD5,
|
||||||
|
ConfigurationVersion: "v1",
|
||||||
|
Default: false,
|
||||||
|
OrgID: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = store.GetLatestAlertmanagerConfiguration(context.Background(), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, newConfig, req.Result.AlertmanagerConfiguration)
|
||||||
|
require.Equal(t, newConfigMD5, req.Result.ConfigurationHash)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("When passing the wrong hash the update should error", func(t *testing.T) {
|
||||||
|
config, configMD5 := setupConfig(t, "my-config")
|
||||||
|
req := &models.GetLatestAlertmanagerConfigurationQuery{
|
||||||
|
OrgID: 1,
|
||||||
|
}
|
||||||
|
err := store.GetLatestAlertmanagerConfiguration(context.Background(), req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, configMD5, req.Result.ConfigurationHash)
|
||||||
|
err = store.UpdateAlertManagerConfiguration(&models.SaveAlertmanagerConfigurationCmd{
|
||||||
|
AlertmanagerConfiguration: config,
|
||||||
|
FetchedConfigurationHash: "the-wrong-hash",
|
||||||
|
ConfigurationVersion: "v1",
|
||||||
|
Default: false,
|
||||||
|
OrgID: 1,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, ErrVersionLockedObjectNotFound, err.Error())
|
||||||
|
})
|
||||||
|
}
|
@ -22,6 +22,7 @@ type AlertingStore interface {
|
|||||||
GetAllLatestAlertmanagerConfiguration(ctx context.Context) ([]*models.AlertConfiguration, error)
|
GetAllLatestAlertmanagerConfiguration(ctx context.Context) ([]*models.AlertConfiguration, error)
|
||||||
SaveAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error
|
SaveAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error
|
||||||
SaveAlertmanagerConfigurationWithCallback(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd, callback SaveCallback) error
|
SaveAlertmanagerConfigurationWithCallback(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd, callback SaveCallback) error
|
||||||
|
UpdateAlertManagerConfiguration(cmd *models.SaveAlertmanagerConfigurationCmd) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBstore stores the alert definitions and instances in the database.
|
// DBstore stores the alert definitions and instances in the database.
|
||||||
|
@ -306,6 +306,10 @@ func AddAlertmanagerConfigMigrations(mg *migrator.Migrator) {
|
|||||||
mg.AddMigration("add index in alert_configuration table on org_id column", migrator.NewAddIndexMigration(alertConfiguration, &migrator.Index{
|
mg.AddMigration("add index in alert_configuration table on org_id column", migrator.NewAddIndexMigration(alertConfiguration, &migrator.Index{
|
||||||
Cols: []string{"org_id"},
|
Cols: []string{"org_id"},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("add configuration_hash column to alert_configuration", migrator.NewAddColumnMigration(alertConfiguration, &migrator.Column{
|
||||||
|
Name: "configuration_hash", Type: migrator.DB_Varchar, Nullable: false, Default: "'not-yet-calculated'", Length: 32,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {
|
func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {
|
||||||
|
Loading…
Reference in New Issue
Block a user