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:
Alexander Weaver 2022-02-04 13:23:19 -06:00 committed by GitHub
parent 7105bb3be7
commit 935059a376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 218 additions and 0 deletions

View File

@ -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'"`

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

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

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

View File

@ -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]))
}