Alerting: Implement setting of version message in convert API. (#114331)
Part 2 of adding version messages to the `alert_rule_version` able. This allows setting the message via a header when using the Prometheus conversion API, which can be useful for e.g. linking changes back to source control.
This commit is contained in:
@@ -63,6 +63,9 @@ const (
|
||||
// configIdentifierHeader is the header that specifies the identifier for imported Alertmanager config.
|
||||
configIdentifierHeader = "X-Grafana-Alerting-Config-Identifier"
|
||||
defaultConfigIdentifier = "default"
|
||||
|
||||
// versionMessageHeader is the header that specifies an optional message for rule versions.
|
||||
versionMessageHeader = "X-Grafana-Alerting-Version-Message"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -407,6 +410,11 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
versionMessage, err := parseVersionMessageHeader(c)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse version message header", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
// 2. Convert Prometheus Rules to GMA
|
||||
grafanaGroups := make([]*models.AlertRuleGroup, 0, len(promNamespaces))
|
||||
for ns, rgs := range promNamespaces {
|
||||
@@ -444,12 +452,13 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroups(c *context
|
||||
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
grafanaGroups = append(grafanaGroups, grafanaGroup)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update the GMA Rules in the DB
|
||||
err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance)
|
||||
err = srv.alertRuleService.ReplaceRuleGroups(c.Req.Context(), c.SignedInUser, grafanaGroups, provenance, versionMessage)
|
||||
if err != nil {
|
||||
logger.Error("Failed to replace rule groups", "error", err)
|
||||
return errorToResponse(err)
|
||||
@@ -856,6 +865,16 @@ func parseExtraLabelsHeader(c *contextmodel.ReqContext) (map[string]string, erro
|
||||
return parseKeyValuePairs(labelsStr, extraLabelsHeader)
|
||||
}
|
||||
|
||||
// parseVersionMessageHeader obtains and validates the message header value.
|
||||
func parseVersionMessageHeader(c *contextmodel.ReqContext) (string, error) {
|
||||
str := strings.TrimSpace(c.Req.Header.Get(versionMessageHeader))
|
||||
// Limit message to the same as the dashboards message.
|
||||
if len(str) > 500 {
|
||||
return "", errInvalidHeaderValue(versionMessageHeader, errors.New("must be less than 500 characters"))
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
func formatMergeMatchers(matchers amconfig.Matchers) string {
|
||||
var pairs []string
|
||||
for _, matcher := range matchers {
|
||||
|
||||
@@ -506,6 +506,68 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with version message header should pass message to rule store", func(t *testing.T) {
|
||||
provenanceStore := fakes.NewFakeProvisioningStore()
|
||||
folderService := foldertest.NewFakeService()
|
||||
srv, _, ruleStore := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore), withFolderService(folderService))
|
||||
|
||||
// Create a folder in the root
|
||||
fldr := randFolder()
|
||||
fldr.ParentUID = ""
|
||||
folderService.ExpectedFolder = fldr
|
||||
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
||||
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
||||
|
||||
makeGroup := func(alertname string) apimodels.PrometheusRuleGroup {
|
||||
return apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: alertname,
|
||||
Expr: "up == 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create a rule with the X-Grafana-Alerting-Version-Message to check it's passed to InsertRule.
|
||||
|
||||
rc1 := createRequestCtx()
|
||||
rc1.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #1")
|
||||
response1 := srv.RouteConvertPrometheusPostRuleGroup(rc1, fldr.Title, makeGroup("AlertV1"))
|
||||
require.Equal(t, http.StatusAccepted, response1.Status())
|
||||
|
||||
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
||||
a, ok := cmd.([]models.InsertRule)
|
||||
return a, ok
|
||||
})
|
||||
require.Len(t, inserts, 1)
|
||||
cmd1 := inserts[0].([]models.InsertRule)
|
||||
require.Len(t, cmd1, 1)
|
||||
require.Equal(t, "version #1", cmd1[0].Message)
|
||||
|
||||
// Now update the rule to check it gets passed to UpdateRule.
|
||||
|
||||
rc2 := createRequestCtx()
|
||||
rc2.Req.Header.Set("X-Grafana-Alerting-Version-Message", "version #2")
|
||||
response2 := srv.RouteConvertPrometheusPostRuleGroup(rc2, fldr.Title, makeGroup("AlertV2"))
|
||||
require.Equal(t, http.StatusAccepted, response2.Status())
|
||||
|
||||
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
||||
a, ok := cmd.([]models.UpdateRule)
|
||||
return a, ok
|
||||
})
|
||||
require.Len(t, updates, 1)
|
||||
cmd2 := updates[0].([]models.UpdateRule)
|
||||
require.Len(t, cmd2, 1)
|
||||
require.Equal(t, "version #2", cmd2[0].Message)
|
||||
})
|
||||
|
||||
t.Run("returns error when target datasource does not exist", func(t *testing.T) {
|
||||
srv, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
@@ -75,7 +75,7 @@ type AlertRuleService interface {
|
||||
UpdateAlertRule(ctx context.Context, user identity.Requester, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error)
|
||||
DeleteAlertRule(ctx context.Context, user identity.Requester, ruleUID string, provenance alerting_models.Provenance) error
|
||||
GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error)
|
||||
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error
|
||||
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance, message string) error
|
||||
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error
|
||||
DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error
|
||||
GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error)
|
||||
@@ -492,7 +492,10 @@ func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *contextmodel.ReqContext, a
|
||||
ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
provenance := determineProvenance(c)
|
||||
err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance))
|
||||
// TODO: https://github.com/grafana/grafana/issues/114197
|
||||
// Support passing change messages.
|
||||
changeMessage := ""
|
||||
err = srv.alertRules.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, groupModel, alerting_models.Provenance(provenance), changeMessage)
|
||||
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
|
||||
@@ -576,9 +576,9 @@ func (srv RulerSrv) performUpdateAlertRules(ctx context.Context, c *contextmodel
|
||||
}
|
||||
|
||||
if len(finalChanges.New) > 0 {
|
||||
inserts := make([]ngmodels.AlertRule, 0, len(finalChanges.New))
|
||||
inserts := make([]ngmodels.InsertRule, 0, len(finalChanges.New))
|
||||
for _, rule := range finalChanges.New {
|
||||
inserts = append(inserts, *rule)
|
||||
inserts = append(inserts, ngmodels.InsertRule{AlertRule: *rule})
|
||||
}
|
||||
added, err := srv.store.InsertAlertRules(tranCtx, ngmodels.NewUserUID(c.SignedInUser), inserts)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,7 +28,7 @@ type RuleStore interface {
|
||||
|
||||
// InsertAlertRules will insert all alert rules passed into the function
|
||||
// and return the map of uuid to id.
|
||||
InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.AlertRule) ([]ngmodels.AlertRuleKeyWithId, error)
|
||||
InsertAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.InsertRule) ([]ngmodels.AlertRuleKeyWithId, error)
|
||||
UpdateAlertRules(ctx context.Context, user *ngmodels.UserUID, rules []ngmodels.UpdateRule) error
|
||||
DeleteAlertRulesByUID(ctx context.Context, orgID int64, user *ngmodels.UserUID, permanently bool, ruleUID ...string) error
|
||||
DeleteRuleFromTrashByGUID(ctx context.Context, orgID int64, ruleGUID string) (int64, error)
|
||||
|
||||
Reference in New Issue
Block a user