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:
Jean-Philippe Quéméner 2022-03-23 09:31:46 +01:00 committed by GitHub
parent ff3c1e3144
commit a80f04c949
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 0 deletions

View File

@ -7,6 +7,7 @@ type AlertConfiguration struct {
ID int64 `xorm:"pk autoincr 'id'"`
AlertmanagerConfiguration string
ConfigurationHash string
ConfigurationVersion string
CreatedAt int64 `xorm:"created"`
Default bool
@ -22,6 +23,7 @@ type GetLatestAlertmanagerConfigurationQuery struct {
// SaveAlertmanagerConfigurationCmd is the command to save an alertmanager configuration.
type SaveAlertmanagerConfigurationCmd struct {
AlertmanagerConfiguration string
FetchedConfigurationHash string
ConfigurationVersion string
Default bool
OrgID int64

View File

@ -2,6 +2,9 @@ package notifier
import (
"context"
"crypto/md5"
"errors"
"fmt"
"strings"
"sync"
"testing"
@ -67,6 +70,20 @@ func (f *FakeConfigStore) SaveAlertmanagerConfigurationWithCallback(_ context.Co
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 {
orgs []int64
}

View File

@ -2,6 +2,7 @@ package store
import (
"context"
"crypto/md5"
"fmt"
"xorm.io/builder"
@ -13,6 +14,9 @@ import (
var (
// ErrNoAlertmanagerConfiguration is an error for when no alertmanager configuration is found.
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.
@ -64,6 +68,7 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(ctx context.Context,
return st.SQLStore.WithTransactionalDbSession(ctx, 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,
@ -79,3 +84,31 @@ func (st DBstore) SaveAlertmanagerConfigurationWithCallback(ctx context.Context,
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
})
}

View 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())
})
}

View File

@ -22,6 +22,7 @@ type AlertingStore interface {
GetAllLatestAlertmanagerConfiguration(ctx context.Context) ([]*models.AlertConfiguration, error)
SaveAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) 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.

View File

@ -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{
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) {