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
|
||||
}
|
||||
|
||||
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.
|
||||
type AlertRuleVersion struct {
|
||||
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
|
||||
AddAlertAdminConfigMigrations(mg)
|
||||
|
||||
// Create provisioning data table
|
||||
AddProvisioningMigrations(mg)
|
||||
}
|
||||
|
||||
// AddAlertDefinitionMigrations should not be modified.
|
||||
@ -327,3 +330,22 @@ func AddAlertAdminConfigMigrations(mg *migrator.Migrator) {
|
||||
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