mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Create basic storage layer for provisioning (#44679)
* Simplistic store API for provenance lookups on arbitrary types * Add a few notes in comments * Improved type safety for provisioned objects * Clean-up TODOs for future PRs * Clean up provisioning model * Clean up tests * Restrict allowable types in interface * Fix linter error * Move AlertRule domain methods to same file as AlertRule definition * Update pkg/services/ngalert/models/provisioning.go Co-authored-by: George Robinson <george.robinson@grafana.com> * Complete interface rename * Pass context through store API * More idiomatic method names * Better error description * Improve code-docs * Use ORM language instead of raw sql * Add support for records in different orgs * ResourceTypeID -> ResourceType since it's not an ID Co-authored-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
parent
7105bb3be7
commit
935059a376
@ -105,6 +105,18 @@ func (alertRule *AlertRule) PreSave(timeNow func() time.Time) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (alertRule *AlertRule) ResourceType() string {
|
||||||
|
return "alertRule"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (alertRule *AlertRule) ResourceID() string {
|
||||||
|
return alertRule.UID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (alertRule *AlertRule) ResourceOrgID() int64 {
|
||||||
|
return alertRule.OrgID
|
||||||
|
}
|
||||||
|
|
||||||
// AlertRuleVersion is the model for alert rule versions in unified alerting.
|
// AlertRuleVersion is the model for alert rule versions in unified alerting.
|
||||||
type AlertRuleVersion struct {
|
type AlertRuleVersion struct {
|
||||||
ID int64 `xorm:"pk autoincr 'id'"`
|
ID int64 `xorm:"pk autoincr 'id'"`
|
||||||
|
16
pkg/services/ngalert/models/provisioning.go
Normal file
16
pkg/services/ngalert/models/provisioning.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Provenance string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProvenanceNone Provenance = ""
|
||||||
|
ProvenanceApi Provenance = "api"
|
||||||
|
ProvenanceFile Provenance = "file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provisionable represents a resource that can be created through a provisioning mechanism, such as Terraform or config file.
|
||||||
|
type Provisionable interface {
|
||||||
|
ResourceType() string
|
||||||
|
ResourceID() string
|
||||||
|
ResourceOrgID() int64
|
||||||
|
}
|
85
pkg/services/ngalert/store/provisioning_store.go
Normal file
85
pkg/services/ngalert/store/provisioning_store.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provenanceRecord struct {
|
||||||
|
Id int `xorm:"pk autoincr 'id'"`
|
||||||
|
OrgID int64 `xorm:"'org_id'"`
|
||||||
|
RecordKey string
|
||||||
|
RecordType string
|
||||||
|
Provenance models.Provenance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pr provenanceRecord) TableName() string {
|
||||||
|
return "provenance_type"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvisioningStore is a store of provisioning data for arbitrary objects.
|
||||||
|
type ProvisioningStore interface {
|
||||||
|
GetProvenance(ctx context.Context, o models.Provisionable) (models.Provenance, error)
|
||||||
|
// TODO: API to query all provenances for a specific type?
|
||||||
|
SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProvenance gets the provenance status for a provisionable object.
|
||||||
|
func (st DBstore) GetProvenance(ctx context.Context, o models.Provisionable) (models.Provenance, error) {
|
||||||
|
recordType := o.ResourceType()
|
||||||
|
recordKey := o.ResourceID()
|
||||||
|
orgID := o.ResourceOrgID()
|
||||||
|
|
||||||
|
provenance := models.ProvenanceNone
|
||||||
|
err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
filter := "record_key = ? AND record_type = ? AND org_id = ?"
|
||||||
|
var result models.Provenance
|
||||||
|
has, err := sess.Table(provenanceRecord{}).Where(filter, recordKey, recordType, orgID).Desc("id").Cols("provenance").Get(&result)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query for existing provenance status: %w", err)
|
||||||
|
}
|
||||||
|
if has {
|
||||||
|
provenance = result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return models.ProvenanceNone, err
|
||||||
|
}
|
||||||
|
return provenance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProvenance changes the provenance status for a provisionable object.
|
||||||
|
func (st DBstore) SetProvenance(ctx context.Context, o models.Provisionable, p models.Provenance) error {
|
||||||
|
recordType := o.ResourceType()
|
||||||
|
recordKey := o.ResourceID()
|
||||||
|
orgID := o.ResourceOrgID()
|
||||||
|
|
||||||
|
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||||
|
// TODO: Add a unit-of-work pattern, so updating objects + provenance will happen consistently with rollbacks across stores.
|
||||||
|
// TODO: Need to make sure that writing a record where our concurrency key fails will also fail the whole transaction. That way, this gets rolled back too. can't just check that 0 updates happened inmemory. Check with jp. If not possible, we need our own concurrency key.
|
||||||
|
// TODO: Clean up stale provenance records periodically.
|
||||||
|
filter := "record_key = ? AND record_type = ? AND org_id = ?"
|
||||||
|
_, err := sess.Table(provenanceRecord{}).Where(filter, recordKey, recordType, orgID).Delete(provenanceRecord{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete pre-existing provisioning status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := provenanceRecord{
|
||||||
|
RecordKey: recordKey,
|
||||||
|
RecordType: recordType,
|
||||||
|
Provenance: p,
|
||||||
|
OrgID: orgID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := sess.Insert(record); err != nil {
|
||||||
|
return fmt.Errorf("failed to store provisioning status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
83
pkg/services/ngalert/store/provisioning_store_test.go
Normal file
83
pkg/services/ngalert/store/provisioning_store_test.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package store_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/tests"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testAlertingIntervalSeconds = 10
|
||||||
|
|
||||||
|
func TestProvisioningStore(t *testing.T) {
|
||||||
|
_, dbstore := tests.SetupTestEnv(t, testAlertingIntervalSeconds)
|
||||||
|
|
||||||
|
t.Run("Default provenance of a known type is None", func(t *testing.T) {
|
||||||
|
rule := models.AlertRule{
|
||||||
|
UID: "asdf",
|
||||||
|
}
|
||||||
|
|
||||||
|
provenance, err := dbstore.GetProvenance(context.Background(), &rule)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, models.ProvenanceNone, provenance)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Store returns saved provenance type", func(t *testing.T) {
|
||||||
|
rule := models.AlertRule{
|
||||||
|
UID: "123",
|
||||||
|
}
|
||||||
|
err := dbstore.SetProvenance(context.Background(), &rule, models.ProvenanceFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
p, err := dbstore.GetProvenance(context.Background(), &rule)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, models.ProvenanceFile, p)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Store does not get provenance of record with different org ID", func(t *testing.T) {
|
||||||
|
ruleOrg2 := models.AlertRule{
|
||||||
|
UID: "456",
|
||||||
|
OrgID: 2,
|
||||||
|
}
|
||||||
|
ruleOrg3 := models.AlertRule{
|
||||||
|
UID: "456",
|
||||||
|
OrgID: 3,
|
||||||
|
}
|
||||||
|
err := dbstore.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
p, err := dbstore.GetProvenance(context.Background(), &ruleOrg3)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, models.ProvenanceNone, p)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Store only updates provenance of record with given org ID", func(t *testing.T) {
|
||||||
|
ruleOrg2 := models.AlertRule{
|
||||||
|
UID: "789",
|
||||||
|
OrgID: 2,
|
||||||
|
}
|
||||||
|
ruleOrg3 := models.AlertRule{
|
||||||
|
UID: "789",
|
||||||
|
OrgID: 3,
|
||||||
|
}
|
||||||
|
err := dbstore.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = dbstore.SetProvenance(context.Background(), &ruleOrg3, models.ProvenanceFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = dbstore.SetProvenance(context.Background(), &ruleOrg2, models.ProvenanceApi)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
p, err := dbstore.GetProvenance(context.Background(), &ruleOrg2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, models.ProvenanceApi, p)
|
||||||
|
p, err = dbstore.GetProvenance(context.Background(), &ruleOrg3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, models.ProvenanceFile, p)
|
||||||
|
})
|
||||||
|
}
|
@ -22,6 +22,9 @@ func AddTablesMigrations(mg *migrator.Migrator) {
|
|||||||
|
|
||||||
// Create Admin Configuration
|
// Create Admin Configuration
|
||||||
AddAlertAdminConfigMigrations(mg)
|
AddAlertAdminConfigMigrations(mg)
|
||||||
|
|
||||||
|
// Create provisioning data table
|
||||||
|
AddProvisioningMigrations(mg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAlertDefinitionMigrations should not be modified.
|
// AddAlertDefinitionMigrations should not be modified.
|
||||||
@ -327,3 +330,22 @@ func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {
|
|||||||
Name: "send_alerts_to", Type: migrator.DB_SmallInt, Nullable: false,
|
Name: "send_alerts_to", Type: migrator.DB_SmallInt, Nullable: false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddProvisioningMigrations(mg *migrator.Migrator) {
|
||||||
|
provisioningTable := migrator.Table{
|
||||||
|
Name: "provenance_type",
|
||||||
|
Columns: []*migrator.Column{
|
||||||
|
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||||
|
{Name: "org_id", Type: migrator.DB_BigInt, Nullable: false},
|
||||||
|
{Name: "record_key", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "record_type", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
{Name: "provenance", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
|
||||||
|
},
|
||||||
|
Indices: []*migrator.Index{
|
||||||
|
{Cols: []string{"record_type", "record_key", "org_id"}, Type: migrator.UniqueIndex},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.AddMigration("create provenance_type table", migrator.NewAddTableMigration(provisioningTable))
|
||||||
|
mg.AddMigration("add index to uniquify (record_key, record_type, org_id) columns", migrator.NewAddIndexMigration(provisioningTable, provisioningTable.Indices[0]))
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user