Alerting: Fix label escaping in rule export (#97985)

This commit is contained in:
Moustafa Baiou 2025-01-07 17:09:27 -05:00 committed by GitHub
parent f20602ed42
commit 25538bcfdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 218 additions and 0 deletions

View File

@ -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 = &notificationSettings
}
}
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 {

View File

@ -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)
})
})
}

View 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"

View File

@ -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()