From 9e21e4d1c1fdc609ea57a589945af6ec43f4c9e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 28 Apr 2022 21:27:34 +0200 Subject: [PATCH] Alerting: indicate whether an alertrule is provisioned (#48458) --- pkg/services/ngalert/CHANGELOG.md | 1 + pkg/services/ngalert/api/api.go | 2 + pkg/services/ngalert/api/api_ruler.go | 37 ++++++++++++---- pkg/services/ngalert/api/api_ruler_test.go | 44 +++++++++++++++++++ pkg/services/ngalert/api/fork_ruler.go | 2 +- .../api/tooling/definitions/cortex-ruler.go | 1 + pkg/services/ngalert/ngalert.go | 1 + 7 files changed, 79 insertions(+), 9 deletions(-) diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index 2c15f8e0d92..4a259114440 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -52,6 +52,7 @@ Scopes must have an order to ensure consistency and ease of search, this helps u - `grafana_alerting_ticker_interval_seconds` - [FEATURE] Indicate whether routes are provisioned when GETting Alertmanager configuration #47857 - [FEATURE] Indicate whether contact point is provisioned when GETting Alertmanager configuration #48323 +- [FEATURE] Indicate whether alert rule is provisioned when GETting the rule #48458 ## 8.5.1 diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 4562f951d1a..7da24a5d636 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -67,6 +67,7 @@ type API struct { QuotaService *quota.QuotaService Schedule schedule.ScheduleService TransactionManager provisioning.TransactionManager + ProvenanceStore provisioning.ProvisioningStore RuleStore store.RuleStore InstanceStore store.InstanceStore AlertingStore AlertingStore @@ -109,6 +110,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { QuotaService: api.QuotaService, scheduleService: api.Schedule, store: api.RuleStore, + provenanceStore: api.ProvenanceStore, xactManager: api.TransactionManager, log: logger, cfg: &api.Cfg.UnifiedAlerting, diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index f8f9a43a10f..d7a08fe8b4a 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -30,6 +30,7 @@ import ( type RulerSrv struct { xactManager provisioning.TransactionManager + provenanceStore provisioning.ProvisioningStore store store.RuleStore DatasourceCache datasources.CacheService QuotaService *quota.QuotaService @@ -143,6 +144,11 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } + provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get provenance for rule group") + } + for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue @@ -154,11 +160,11 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, namespace.Id), + toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords), }, } } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id)) + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords)) ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig } } @@ -170,7 +176,7 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. return response.JSON(http.StatusAccepted, result) } -func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { +func (srv RulerSrv) RouteGetRulesGroupConfig(c *models.ReqContext) response.Response { namespaceTitle := web.Params(c.Req)[":Namespace"] namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false) if err != nil { @@ -194,12 +200,17 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } + provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules") + } + for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue } ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second) - ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id)) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id, provenanceRecords)) } result := apimodels.RuleGroupConfigResponse{ @@ -255,6 +266,11 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqSignedIn, evaluator) } + provenanceRecords, err := srv.provenanceStore.GetProvenances(c.Req.Context(), c.SignedInUser.OrgId, (&ngmodels.AlertRule{}).ResourceType()) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules") + } + for _, r := range q.Result { if !authorizeDatasourceAccessForRule(r, hasAccess) { continue @@ -273,7 +289,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, folder.Id), + toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords), }, } } else { @@ -284,11 +300,11 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r, folder.Id), + toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords), }, } } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id)) + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id, provenanceRecords)) configs[namespace][r.RuleGroup] = ruleGroupConfig } } @@ -437,7 +453,11 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, namespace *mod return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"}) } -func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimodels.GettableExtendedRuleNode { +func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64, provenanceRecords map[string]ngmodels.Provenance) apimodels.GettableExtendedRuleNode { + provenance := ngmodels.ProvenanceNone + if prov, exists := provenanceRecords[r.ResourceID()]; exists { + provenance = prov + } gettableExtendedRuleNode := apimodels.GettableExtendedRuleNode{ GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ ID: r.ID, @@ -454,6 +474,7 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimode RuleGroup: r.RuleGroup, NoDataState: apimodels.NoDataState(r.NoDataState), ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), + Provenance: provenance, }, } gettableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{ diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 1d10b29c80d..f0bfbb70c0a 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/schedule" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/util" @@ -550,6 +551,48 @@ func TestRouteGetNamespaceRulesConfig(t *testing.T) { assert.Emptyf(t, expectedRules, "not all expected rules were returned") }) }) + t.Run("should return the provenance of the alert rules", func(t *testing.T) { + orgID := rand.Int63() + folder := randFolder() + ruleStore := store.NewFakeRuleStore(t) + ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder) + expectedRules := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder))) + ruleStore.PutRule(context.Background(), expectedRules...) + ac := acMock.New().WithDisabled() + + svc := createService(ac, ruleStore, nil) + + // add provenance to the first generated rule + rule := &models.AlertRule{ + UID: expectedRules[0].UID, + } + err := svc.provenanceStore.SetProvenance(context.Background(), rule, orgID, models.ProvenanceAPI) + require.NoError(t, err) + + response := svc.RouteGetNamespaceRulesConfig(createRequestContext(orgID, "", map[string]string{ + ":Namespace": folder.Title, + })) + + require.Equal(t, http.StatusAccepted, response.Status()) + result := &apimodels.NamespaceConfigResponse{} + require.NoError(t, json.Unmarshal(response.Body(), result)) + require.NotNil(t, result) + found := false + for namespace, groups := range *result { + require.Equal(t, folder.Title, namespace) + for _, group := range groups { + for _, actualRule := range group.Rules { + if actualRule.GrafanaManagedAlert.UID == expectedRules[0].UID { + require.Equal(t, models.ProvenanceAPI, actualRule.GrafanaManagedAlert.Provenance) + found = true + } else { + require.Equal(t, models.ProvenanceNone, actualRule.GrafanaManagedAlert.Provenance) + } + } + } + } + require.True(t, found) + }) } func createService(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedule.ScheduleService) *RulerSrv { @@ -558,6 +601,7 @@ func createService(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedu store: store, DatasourceCache: nil, QuotaService: nil, + provenanceStore: provisioning.NewFakeProvisioningStore(), scheduleService: scheduler, log: log.New("test"), cfg: nil, diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/fork_ruler.go index 69411ba9ce8..d43d31c632c 100644 --- a/pkg/services/ngalert/api/fork_ruler.go +++ b/pkg/services/ngalert/api/fork_ruler.go @@ -122,7 +122,7 @@ func (f *ForkedRulerApi) forkRouteGetNamespaceGrafanaRulesConfig(ctx *models.Req } func (f *ForkedRulerApi) forkRouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { - return f.GrafanaRuler.RouteGetRulegGroupConfig(ctx) + return f.GrafanaRuler.RouteGetRulesGroupConfig(ctx) } func (f *ForkedRulerApi) forkRouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { diff --git a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go index bbc08c9103b..3e913750012 100644 --- a/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go +++ b/pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go @@ -386,4 +386,5 @@ type GettableGrafanaRule struct { RuleGroup string `json:"rule_group" yaml:"rule_group"` NoDataState NoDataState `json:"no_data_state" yaml:"no_data_state"` ExecErrState ExecutionErrorState `json:"exec_err_state" yaml:"exec_err_state"` + Provenance models.Provenance `json:"provenance,omitempty" yaml:"provenance,omitempty"` } diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 477842c86c6..a348491d562 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -156,6 +156,7 @@ func (ng *AlertNG) init() error { RuleStore: store, AlertingStore: store, AdminConfigStore: store, + ProvenanceStore: store, MultiOrgAlertmanager: ng.MultiOrgAlertmanager, StateManager: ng.stateManager, AccessControl: ng.accesscontrol,