From 935059a37648ae114d6754b59c4f5b89efee4865 Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Fri, 4 Feb 2022 13:23:19 -0600 Subject: [PATCH] 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 * 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 --- pkg/services/ngalert/models/alert_rule.go | 12 +++ pkg/services/ngalert/models/provisioning.go | 16 ++++ .../ngalert/store/provisioning_store.go | 85 +++++++++++++++++++ .../ngalert/store/provisioning_store_test.go | 83 ++++++++++++++++++ .../sqlstore/migrations/ualert/tables.go | 22 +++++ 5 files changed, 218 insertions(+) create mode 100644 pkg/services/ngalert/models/provisioning.go create mode 100644 pkg/services/ngalert/store/provisioning_store.go create mode 100644 pkg/services/ngalert/store/provisioning_store_test.go diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index 07c1e13dea1..dd717b3c358 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -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'"` diff --git a/pkg/services/ngalert/models/provisioning.go b/pkg/services/ngalert/models/provisioning.go new file mode 100644 index 00000000000..c17e3de09a1 --- /dev/null +++ b/pkg/services/ngalert/models/provisioning.go @@ -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 +} diff --git a/pkg/services/ngalert/store/provisioning_store.go b/pkg/services/ngalert/store/provisioning_store.go new file mode 100644 index 00000000000..6681a290f58 --- /dev/null +++ b/pkg/services/ngalert/store/provisioning_store.go @@ -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 + }) +} diff --git a/pkg/services/ngalert/store/provisioning_store_test.go b/pkg/services/ngalert/store/provisioning_store_test.go new file mode 100644 index 00000000000..668a4b60630 --- /dev/null +++ b/pkg/services/ngalert/store/provisioning_store_test.go @@ -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) + }) +} diff --git a/pkg/services/sqlstore/migrations/ualert/tables.go b/pkg/services/sqlstore/migrations/ualert/tables.go index 2cb55a3e040..7d872119c4d 100644 --- a/pkg/services/sqlstore/migrations/ualert/tables.go +++ b/pkg/services/sqlstore/migrations/ualert/tables.go @@ -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])) +}