mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix label escaping in rule export (#97985)
This commit is contained in:
parent
f20602ed42
commit
25538bcfdf
@ -572,6 +572,7 @@ func exportResponse(c *contextmodel.ReqContext, body definitions.AlertingFileExp
|
||||
return exportHcl(params.Download, body)
|
||||
}
|
||||
|
||||
body = escapeAlertingFileExport(body)
|
||||
if params.Download {
|
||||
r := response.JSONDownload
|
||||
if params.Format == "yaml" {
|
||||
@ -587,6 +588,65 @@ func exportResponse(c *contextmodel.ReqContext, body definitions.AlertingFileExp
|
||||
return r(http.StatusOK, body)
|
||||
}
|
||||
|
||||
// escape all strings except:
|
||||
// Alert rule annotations: groups[].rules[].annotations
|
||||
// Alert rule time range: groups[].rules[].relativeTimeRange
|
||||
// Alert rule query model: groups[].rules[].data.model
|
||||
// Mute timings name: muteTimes[].name
|
||||
// Mute timings time intervals: muteTimes[].time_intervals[]
|
||||
// Notification template name: templates[].name
|
||||
// Notification template content: templates[].template
|
||||
func escapeAlertingFileExport(body definitions.AlertingFileExport) definitions.AlertingFileExport {
|
||||
for i, group := range body.Groups {
|
||||
body.Groups[i] = escapeRuleGroup(group)
|
||||
}
|
||||
// TODO: implement escaping for the other export fields
|
||||
return body
|
||||
}
|
||||
|
||||
// escape all strings except:
|
||||
// Alert rule annotations: groups[].rules[].annotations
|
||||
// Alert rule time range: groups[].rules[].relativeTimeRange
|
||||
// Alert rule query model: groups[].rules[].data.model
|
||||
func escapeRuleGroup(group definitions.AlertRuleGroupExport) definitions.AlertRuleGroupExport {
|
||||
group.Name = addEscapeCharactersToString(group.Name)
|
||||
group.Folder = addEscapeCharactersToString(group.Folder)
|
||||
for i, rule := range group.Rules {
|
||||
group.Rules[i].Title = addEscapeCharactersToString(rule.Title)
|
||||
if rule.Labels != nil {
|
||||
group.Rules[i].Labels = escapeMapValues(*rule.Labels)
|
||||
}
|
||||
if rule.NotificationSettings != nil {
|
||||
notificationSettings := escapeRuleNotificationSettings(*rule.NotificationSettings)
|
||||
group.Rules[i].NotificationSettings = ¬ificationSettings
|
||||
}
|
||||
}
|
||||
return group
|
||||
}
|
||||
|
||||
func escapeRuleNotificationSettings(ns definitions.AlertRuleNotificationSettingsExport) definitions.AlertRuleNotificationSettingsExport {
|
||||
ns.Receiver = addEscapeCharactersToString(ns.Receiver)
|
||||
for j := range ns.GroupBy {
|
||||
ns.GroupBy[j] = addEscapeCharactersToString(ns.GroupBy[j])
|
||||
}
|
||||
for k := range ns.MuteTimeIntervals {
|
||||
ns.MuteTimeIntervals[k] = addEscapeCharactersToString(ns.MuteTimeIntervals[k])
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func escapeMapValues(m map[string]string) *map[string]string {
|
||||
escapedMap := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
escapedMap[k] = addEscapeCharactersToString(v)
|
||||
}
|
||||
return &escapedMap
|
||||
}
|
||||
|
||||
func addEscapeCharactersToString(s string) string {
|
||||
return strings.ReplaceAll(s, "$", "$$")
|
||||
}
|
||||
|
||||
func exportHcl(download bool, body definitions.AlertingFileExport) response.Response {
|
||||
resources := make([]hcl.Resource, 0, len(body.Groups)+len(body.ContactPoints)+len(body.Policies)+len(body.MuteTimings))
|
||||
convertToResources := func() error {
|
||||
|
@ -6,6 +6,9 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -17,6 +20,7 @@ import (
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
@ -809,3 +813,68 @@ func createTestRequest(method string, url string, user string, body string) *htt
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func TestIntegrationExportFileProvision(t *testing.T) {
|
||||
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
|
||||
provisioningDir := filepath.Join(dir, "conf", "provisioning")
|
||||
alertingDir := filepath.Join(provisioningDir, "alerting")
|
||||
err := os.MkdirAll(alertingDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
|
||||
|
||||
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
Password: "admin",
|
||||
Login: "admin",
|
||||
IsAdmin: true,
|
||||
})
|
||||
|
||||
apiClient.ReloadCachedPermissions(t)
|
||||
t.Run("when provisioning alert rules from files", func(t *testing.T) {
|
||||
// add file provisioned alert rules
|
||||
fileProvisionedAlertRules, err := testData.ReadFile(path.Join("test-data", "provisioning-rules.yaml"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var expected definitions.AlertingFileExport
|
||||
require.NoError(t, yaml.Unmarshal(fileProvisionedAlertRules, &expected))
|
||||
expectedYaml, err := yaml.Marshal(expected)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create folder
|
||||
folderUID := "my_first_folder_uid"
|
||||
apiClient.CreateFolder(t, folderUID, "my_first_folder_with_$escaped_symbols")
|
||||
|
||||
err = os.WriteFile(filepath.Join(alertingDir, "provisioning-rules.yaml"), fileProvisionedAlertRules, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
apiClient.ReloadAlertingFileProvisioning(t)
|
||||
|
||||
data, status, _ := apiClient.GetAllRulesWithStatus(t)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
require.Greater(t, len(data), 0)
|
||||
|
||||
t.Run("exported alert rules should escape $ characters", func(t *testing.T) {
|
||||
// call export endpoint
|
||||
status, exportRaw := apiClient.ExportRulesWithStatus(t, &definitions.AlertRulesExportParameters{
|
||||
ExportQueryParams: definitions.ExportQueryParams{Format: "yaml"},
|
||||
FolderUID: []string{folderUID},
|
||||
GroupName: "my_rule_group",
|
||||
})
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
var export definitions.AlertingFileExport
|
||||
require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export))
|
||||
|
||||
// verify the file exported matches the file provisioned thing
|
||||
require.Len(t, export.Groups, 1)
|
||||
require.YAMLEq(t, string(expectedYaml), exportRaw)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
76
pkg/tests/api/alerting/test-data/provisioning-rules.yaml
Normal file
76
pkg/tests/api/alerting/test-data/provisioning-rules.yaml
Normal file
@ -0,0 +1,76 @@
|
||||
# config file version
|
||||
apiVersion: 1
|
||||
|
||||
# ONLY THESE PATHS ARE NOT TEMPLATED and therefore don't need escaping:
|
||||
# Alert rule annotations: groups[].rules[].annotations
|
||||
# Alert rule time range: groups[].rules[].relativeTimeRange
|
||||
# Alert rule query model: groups[].rules[].data.model
|
||||
groups:
|
||||
# <int> organization ID, default = 1
|
||||
- orgId: 1
|
||||
# <string, required> name of the rule group
|
||||
name: my_rule_group
|
||||
# <string, required> name of the folder the rule group will be stored in
|
||||
folder: my_first_folder_with_$$escaped_symbols
|
||||
# <duration, required> interval that the rule group should evaluated at
|
||||
interval: 60s
|
||||
# <list, required> list of rules that are part of the rule group
|
||||
rules:
|
||||
# <string, required> unique identifier for the rule. Should not exceed 40 symbols. Only letters, numbers, - (hyphen), and _ (underscore) allowed.
|
||||
- uid: my_id_1
|
||||
# <string, required> title of the rule that will be displayed in the UI
|
||||
title: my_first_rule_with_$$escaped_symbols
|
||||
# <string, required> which query should be used for the condition
|
||||
condition: A
|
||||
# <list, required> list of query objects that should be executed on each
|
||||
# evaluation - should be obtained through the API
|
||||
data:
|
||||
- refId: A
|
||||
datasourceUid: "__expr__"
|
||||
model:
|
||||
conditions:
|
||||
- evaluator:
|
||||
params:
|
||||
- 3
|
||||
type: gt
|
||||
operator:
|
||||
type: and
|
||||
query:
|
||||
params:
|
||||
- A
|
||||
reducer:
|
||||
type: last
|
||||
type: query
|
||||
datasource:
|
||||
type: __expr__
|
||||
uid: "__expr__"
|
||||
expression: 1==0
|
||||
intervalMs: 1000
|
||||
maxDataPoints: 43200
|
||||
refId: A
|
||||
type: math
|
||||
# <string> UID of a dashboard that the alert rule should be linked to
|
||||
dashboardUid: my_dashboard
|
||||
# <int> ID of the panel that the alert rule should be linked to
|
||||
panelId: 123
|
||||
# <string> the state the alert rule will have when no data is returned
|
||||
# possible values: "NoData", "Alerting", "OK", default = NoData
|
||||
noDataState: Alerting
|
||||
# <string> the state the alert rule will have when the query execution
|
||||
# failed - possible values: "Error", "Alerting", "OK"
|
||||
# default = Alerting
|
||||
execErrState: Alerting
|
||||
# <duration, required> for how long should the alert fire before alerting
|
||||
for: 60s
|
||||
# <map<string, string>> a map of strings to pass around any data
|
||||
annotations:
|
||||
some_key: some_value
|
||||
$no_escaping_needed: $no_escaping_needed
|
||||
# <map<string, string> a map of strings that can be used to filter and
|
||||
# route alerts
|
||||
labels:
|
||||
team: sre_team_1
|
||||
label_keys_not_$escaped: $$escaped_value
|
||||
something: "escaped in the middle of things $$value"
|
||||
templated: "{{ $$labels.team }}"
|
||||
middle: "u$$ing_escaped_symbols"
|
@ -337,6 +337,19 @@ func (a apiClient) CreateFolder(t *testing.T, uID string, title string, parentUI
|
||||
a.ReloadCachedPermissions(t)
|
||||
}
|
||||
|
||||
func (a apiClient) ReloadAlertingFileProvisioning(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
u := fmt.Sprintf("%s/api/admin/provisioning/alerting/reload", a.url)
|
||||
// nolint:gosec
|
||||
resp, err := http.Post(u, "application/json", nil)
|
||||
defer func() {
|
||||
require.NoError(t, resp.Body.Close())
|
||||
}()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func (a apiClient) GetOrgQuotaLimits(t *testing.T, orgID int64) (int64, int64) {
|
||||
t.Helper()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user