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)
|
return exportHcl(params.Download, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body = escapeAlertingFileExport(body)
|
||||||
if params.Download {
|
if params.Download {
|
||||||
r := response.JSONDownload
|
r := response.JSONDownload
|
||||||
if params.Format == "yaml" {
|
if params.Format == "yaml" {
|
||||||
@ -587,6 +588,65 @@ func exportResponse(c *contextmodel.ReqContext, body definitions.AlertingFileExp
|
|||||||
return r(http.StatusOK, body)
|
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 {
|
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))
|
resources := make([]hcl.Resource, 0, len(body.Groups)+len(body.ContactPoints)+len(body.Policies)+len(body.MuteTimings))
|
||||||
convertToResources := func() error {
|
convertToResources := func() error {
|
||||||
|
@ -6,6 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
@ -17,6 +20,7 @@ import (
|
|||||||
"github.com/prometheus/common/model"
|
"github.com/prometheus/common/model"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||||
"github.com/grafana/grafana/pkg/expr"
|
"github.com/grafana/grafana/pkg/expr"
|
||||||
@ -809,3 +813,68 @@ func createTestRequest(method string, url string, user string, body string) *htt
|
|||||||
}
|
}
|
||||||
return req
|
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)
|
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) {
|
func (a apiClient) GetOrgQuotaLimits(t *testing.T, orgID int64) (int64, int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user