Alerting: Create endpoints for exporting in provisioning file format (#58623)

This adds provisioning endpoints for downloading alert rules and alert rule groups in a 
format that is compatible with file provisioning. Each endpoint supports both json and 
yaml response types via Accept header as well as a query parameter 
download=true/false that will set Content-Disposition to recommend initiating a download 
or inline display.

This also makes some package changes to keep structs with potential to drift closer 
together. Eventually, other alerting file structs should also move into this new file 
package, but the rest require some refactoring that is out of scope for this PR.
This commit is contained in:
Matthew Jacobson
2023-01-27 11:39:16 -05:00
committed by GitHub
parent d5294eb8fa
commit c006df375a
21 changed files with 2507 additions and 392 deletions

View File

@@ -1,4 +1,4 @@
package alerting
package file
import (
"encoding/json"
@@ -31,11 +31,11 @@ type AlertRuleGroupV1 struct {
Rules []AlertRuleV1 `json:"rules" yaml:"rules"`
}
func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) {
ruleGroup := AlertRuleGroup{}
ruleGroup.Name = ruleGroupV1.Name.Value()
if strings.TrimSpace(ruleGroup.Name) == "" {
return AlertRuleGroup{}, errors.New("rule group has no name set")
func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroupWithFolderTitle, error) {
ruleGroup := AlertRuleGroupWithFolderTitle{AlertRuleGroup: &models.AlertRuleGroup{}}
ruleGroup.Title = ruleGroupV1.Name.Value()
if strings.TrimSpace(ruleGroup.Title) == "" {
return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no name set")
}
ruleGroup.OrgID = ruleGroupV1.OrgID.Value()
if ruleGroup.OrgID < 1 {
@@ -43,29 +43,27 @@ func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) {
}
interval, err := model.ParseDuration(ruleGroupV1.Interval.Value())
if err != nil {
return AlertRuleGroup{}, err
return AlertRuleGroupWithFolderTitle{}, err
}
ruleGroup.Interval = time.Duration(interval)
ruleGroup.Folder = ruleGroupV1.Folder.Value()
if strings.TrimSpace(ruleGroup.Folder) == "" {
return AlertRuleGroup{}, errors.New("rule group has no folder set")
ruleGroup.Interval = int64(time.Duration(interval).Seconds())
ruleGroup.FolderTitle = ruleGroupV1.Folder.Value()
if strings.TrimSpace(ruleGroup.FolderTitle) == "" {
return AlertRuleGroupWithFolderTitle{}, errors.New("rule group has no folder set")
}
for _, ruleV1 := range ruleGroupV1.Rules {
rule, err := ruleV1.mapToModel(ruleGroup.OrgID)
if err != nil {
return AlertRuleGroup{}, err
return AlertRuleGroupWithFolderTitle{}, err
}
ruleGroup.Rules = append(ruleGroup.Rules, rule)
}
return ruleGroup, nil
}
type AlertRuleGroup struct {
OrgID int64
Name string
Folder string
Interval time.Duration
Rules []models.AlertRule
type AlertRuleGroupWithFolderTitle struct {
*models.AlertRuleGroup
OrgID int64
FolderTitle string
}
type AlertRuleV1 struct {
@@ -175,3 +173,130 @@ func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) {
Model: rawMessage,
}, nil
}
// Response structs
// AlertingFileExport is the full provisioned file export.
// swagger:model
type AlertingFileExport struct {
APIVersion int64 `json:"apiVersion" yaml:"apiVersion"`
Groups []AlertRuleGroupExport `json:"groups" yaml:"groups"`
}
// AlertRuleGroupExport is the provisioned file export of AlertRuleGroupV1.
type AlertRuleGroupExport struct {
OrgID int64 `json:"orgId" yaml:"orgId"`
Name string `json:"name" yaml:"name"`
Folder string `json:"folder" yaml:"folder"`
Interval model.Duration `json:"interval" yaml:"interval"`
Rules []AlertRuleExport `json:"rules" yaml:"rules"`
}
// AlertRuleExport is the provisioned file export of models.AlertRule.
type AlertRuleExport struct {
UID string `json:"uid" yaml:"uid"`
Title string `json:"title" yaml:"title"`
Condition string `json:"condition" yaml:"condition"`
Data []AlertQueryExport `json:"data" yaml:"data"`
DashboardUID string `json:"dasboardUid,omitempty" yaml:"dashboardUid,omitempty"`
PanelID int64 `json:"panelId,omitempty" yaml:"panelId,omitempty"`
NoDataState models.NoDataState `json:"noDataState" yaml:"noDataState"`
ExecErrState models.ExecutionErrorState `json:"execErrState" yaml:"execErrState"`
For model.Duration `json:"for" yaml:"for"`
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"`
}
// AlertQueryExport is the provisioned export of models.AlertQuery.
type AlertQueryExport struct {
RefID string `json:"refId" yaml:"refId"`
QueryType string `json:"queryType,omitempty" yaml:"queryType,omitempty"`
RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange,omitempty" yaml:"relativeTimeRange,omitempty"`
DatasourceUID string `json:"datasourceUid" yaml:"datasourceUid"`
Model map[string]interface{} `json:"model" yaml:"model"`
}
// NewAlertingFileExport creates an AlertingFileExport DTO from []AlertRuleGroupWithFolderTitle.
func NewAlertingFileExport(groups []AlertRuleGroupWithFolderTitle) (AlertingFileExport, error) {
f := AlertingFileExport{APIVersion: 1}
for _, group := range groups {
export, err := newAlertRuleGroupExport(group)
if err != nil {
return AlertingFileExport{}, err
}
f.Groups = append(f.Groups, export)
}
return f, nil
}
// newAlertRuleGroupExport creates a AlertRuleGroupExport DTO from models.AlertRuleGroup.
func newAlertRuleGroupExport(d AlertRuleGroupWithFolderTitle) (AlertRuleGroupExport, error) {
rules := make([]AlertRuleExport, 0, len(d.Rules))
for i := range d.Rules {
alert, err := newAlertRuleExport(d.Rules[i])
if err != nil {
return AlertRuleGroupExport{}, err
}
rules = append(rules, alert)
}
return AlertRuleGroupExport{
OrgID: d.OrgID,
Name: d.Title,
Folder: d.FolderTitle,
Interval: model.Duration(time.Duration(d.Interval) * time.Second),
Rules: rules,
}, nil
}
// newAlertRuleExport creates a AlertRuleExport DTO from models.AlertRule.
func newAlertRuleExport(rule models.AlertRule) (AlertRuleExport, error) {
data := make([]AlertQueryExport, 0, len(rule.Data))
for i := range rule.Data {
query, err := newAlertQueryExport(rule.Data[i])
if err != nil {
return AlertRuleExport{}, err
}
data = append(data, query)
}
var dashboardUID string
if rule.DashboardUID != nil {
dashboardUID = *rule.DashboardUID
}
var panelID int64
if rule.PanelID != nil {
panelID = *rule.PanelID
}
return AlertRuleExport{
UID: rule.UID,
Title: rule.Title,
For: model.Duration(rule.For),
Condition: rule.Condition,
Data: data,
DashboardUID: dashboardUID,
PanelID: panelID,
NoDataState: rule.NoDataState,
ExecErrState: rule.ExecErrState,
Annotations: rule.Annotations,
Labels: rule.Labels,
}, nil
}
// newAlertQueryExport creates a AlertQueryExport DTO from models.AlertQuery.
func newAlertQueryExport(query models.AlertQuery) (AlertQueryExport, error) {
// We unmarshal the json.RawMessage model into a map in order to facilitate yaml marshalling.
var mdl map[string]interface{}
err := json.Unmarshal(query.Model, &mdl)
if err != nil {
return AlertQueryExport{}, err
}
return AlertQueryExport{
RefID: query.RefID,
QueryType: query.QueryType,
RelativeTimeRange: query.RelativeTimeRange,
DatasourceUID: query.DatasourceUID,
Model: mdl,
}, nil
}

View File

@@ -1,13 +1,14 @@
package alerting
package file
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
func TestRuleGroup(t *testing.T) {
@@ -60,7 +61,7 @@ func TestRuleGroup(t *testing.T) {
rg.Interval = interval
rgMapped, err := rg.MapToModel()
require.NoError(t, err)
require.Equal(t, 48*time.Hour, rgMapped.Interval)
require.Equal(t, int64(48*time.Hour/time.Second), rgMapped.Interval)
})
t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) {
rg := validRuleGroupV1(t)

View File

@@ -41,24 +41,24 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
files []*AlertingFile) error {
for _, file := range files {
for _, group := range file.Groups {
folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID)
folderUID, err := prov.getOrCreateFolderUID(ctx, group.FolderTitle, group.OrgID)
if err != nil {
return err
}
prov.logger.Debug("provisioning alert rule group",
"org", group.OrgID,
"folder", group.Folder,
"folder", group.FolderTitle,
"folderUID", folderUID,
"name", group.Name)
"name", group.Title)
for _, rule := range group.Rules {
rule.NamespaceUID = folderUID
rule.RuleGroup = group.Name
err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID)
rule.RuleGroup = group.Title
err = prov.provisionRule(ctx, group.OrgID, rule)
if err != nil {
return err
}
}
err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds()))
err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Title, group.Interval)
if err != nil {
return err
}
@@ -77,9 +77,7 @@ func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
func (prov *defaultAlertRuleProvisioner) provisionRule(
ctx context.Context,
orgID int64,
rule alert_models.AlertRule,
folder,
folderUID string) error {
rule alert_models.AlertRule) error {
prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID)
_, _, err := prov.ruleService.GetAlertRule(ctx, orgID, rule.UID)
if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) {

View File

@@ -3,6 +3,7 @@ package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/services/provisioning/alerting/file"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
@@ -15,8 +16,8 @@ type OrgID int64
type AlertingFile struct {
configVersion
Filename string
Groups []AlertRuleGroup
DeleteRules []RuleDelete
Groups []file.AlertRuleGroupWithFolderTitle
DeleteRules []file.RuleDelete
ContactPoints []ContactPoint
DeleteContactPoints []DeleteContactPoint
Policies []NotificiationPolicy
@@ -30,8 +31,8 @@ type AlertingFile struct {
type AlertingFileV1 struct {
configVersion
Filename string
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
Groups []file.AlertRuleGroupV1 `json:"groups" yaml:"groups"`
DeleteRules []file.RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"`
DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"`
Policies []NotificiationPolicyV1 `json:"policies" yaml:"policies"`
@@ -132,7 +133,7 @@ func (fileV1 *AlertingFileV1) mapRules(alertingFile *AlertingFile) error {
if orgID < 1 {
orgID = 1
}
ruleDelete := RuleDelete{
ruleDelete := file.RuleDelete{
UID: ruleDeleteV1.UID.Value(),
OrgID: orgID,
}

View File

@@ -269,6 +269,7 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error
ruleService := provisioning.NewAlertRuleService(
st,
st,
ps.dashboardService,
ps.quotaService,
ps.SQLStore,
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),