From 563fcb8bf4b354eaeaf9ab1c6136cf829e35e52e Mon Sep 17 00:00:00 2001 From: William Wernert Date: Tue, 14 May 2024 09:29:50 -0400 Subject: [PATCH] Alerting: Encode query model map to string in rule export to avoid html escape sequences (#87663) * Encode query model map to string to avoid html escape sequences * Remove insignificant whitespace in test request --- .../ngalert/api/api_ruler_export_test.go | 14 ++++++++++++- pkg/services/ngalert/api/compat.go | 20 ++++++++++++++++++- .../test-data/post-rulegroup-101-export.hcl | 10 +++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/pkg/services/ngalert/api/api_ruler_export_test.go b/pkg/services/ngalert/api/api_ruler_export_test.go index b1281045a00..665daa4441b 100644 --- a/pkg/services/ngalert/api/api_ruler_export_test.go +++ b/pkg/services/ngalert/api/api_ruler_export_test.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "context" "embed" "encoding/json" @@ -42,8 +43,12 @@ func TestExportFromPayload(t *testing.T) { requestFile := "post-rulegroup-101.json" rawBody, err := testData.ReadFile(path.Join("test-data", requestFile)) require.NoError(t, err) + // compact the json to remove any extra whitespace + var buf bytes.Buffer + require.NoError(t, json.Compact(&buf, rawBody)) + // unmarshal the compacted json var body apimodels.PostableRuleGroupConfig - require.NoError(t, json.Unmarshal(rawBody, &body)) + require.NoError(t, json.Unmarshal(buf.Bytes(), &body)) createRequest := func() *contextmodel.ReqContext { return createRequestContextWithPerms(orgID, map[int64]map[string][]string{}, nil) @@ -212,6 +217,13 @@ func TestExportRules(t *testing.T) { gen := ngmodels.RuleGen accessQuery := gen.GenerateQuery() noAccessQuery := gen.GenerateQuery() + mdl := map[string]any{ + "foo": "bar", + "baz": "a <=> b", // explicitly check greater/less than characters + } + model, err := json.Marshal(mdl) + require.NoError(t, err) + accessQuery.Model = model hasAccess1 := gen.With(gen.WithGroupKey(hasAccessKey1), gen.WithQuery(accessQuery), gen.WithUniqueGroupIndex()).GenerateManyRef(5) ruleStore.PutRule(context.Background(), hasAccess1...) diff --git a/pkg/services/ngalert/api/compat.go b/pkg/services/ngalert/api/compat.go index a0c2390254c..5c0ea9fedc2 100644 --- a/pkg/services/ngalert/api/compat.go +++ b/pkg/services/ngalert/api/compat.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "encoding/json" "time" @@ -204,6 +205,17 @@ func AlertRuleExportFromAlertRule(rule models.AlertRule) (definitions.AlertRuleE return result, nil } +func encodeQueryModel(m map[string]any) (string, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + err := enc.Encode(m) + if err != nil { + return "", err + } + return string(bytes.TrimRight(buf.Bytes(), "\n")), nil +} + // AlertQueryExportFromAlertQuery creates a definitions.AlertQueryExport DTO from models.AlertQuery. func AlertQueryExportFromAlertQuery(query models.AlertQuery) (definitions.AlertQueryExport, error) { // We unmarshal the json.RawMessage model into a map in order to facilitate yaml marshalling. @@ -216,6 +228,12 @@ func AlertQueryExportFromAlertQuery(query models.AlertQuery) (definitions.AlertQ if query.QueryType != "" { queryType = &query.QueryType } + + modelString, err := encodeQueryModel(mdl) + if err != nil { + return definitions.AlertQueryExport{}, err + } + return definitions.AlertQueryExport{ RefID: query.RefID, QueryType: queryType, @@ -225,7 +243,7 @@ func AlertQueryExportFromAlertQuery(query models.AlertQuery) (definitions.AlertQ }, DatasourceUID: query.DatasourceUID, Model: mdl, - ModelString: string(query.Model), + ModelString: modelString, }, nil } diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl index 142aead24d0..0b4ed6338f5 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl @@ -17,7 +17,7 @@ resource "grafana_rule_group" "rule_group_0000" { } datasource_uid = "000000002" - model = "{\n \"expr\": \"http_request_duration_microseconds_count\",\n \"hide\": false,\n \"interval\": \"\",\n \"intervalMs\": 1000,\n \"legendFormat\": \"\",\n \"maxDataPoints\": 100,\n \"refId\": \"query\"\n }" + model = "{\"expr\":\"http_request_duration_microseconds_count\",\"hide\":false,\"interval\":\"\",\"intervalMs\":1000,\"legendFormat\":\"\",\"maxDataPoints\":100,\"refId\":\"query\"}" } data { ref_id = "reduced" @@ -28,7 +28,7 @@ resource "grafana_rule_group" "rule_group_0000" { } datasource_uid = "__expr__" - model = "{\n \"expression\": \"query\",\n \"hide\": false,\n \"intervalMs\": 1000,\n \"maxDataPoints\": 100,\n \"reducer\": \"mean\",\n \"refId\": \"reduced\",\n \"type\": \"reduce\"\n }" + model = "{\"expression\":\"query\",\"hide\":false,\"intervalMs\":1000,\"maxDataPoints\":100,\"reducer\":\"mean\",\"refId\":\"reduced\",\"type\":\"reduce\"}" } data { ref_id = "condition" @@ -39,7 +39,7 @@ resource "grafana_rule_group" "rule_group_0000" { } datasource_uid = "__expr__" - model = "{\n \"expression\": \"$reduced > 10\",\n \"hide\": false,\n \"intervalMs\": 1000,\n \"maxDataPoints\": 100,\n \"refId\": \"condition\",\n \"type\": \"math\"\n }" + model = "{\"expression\":\"$reduced > 10\",\"hide\":false,\"intervalMs\":1000,\"maxDataPoints\":100,\"refId\":\"condition\",\"type\":\"math\"}" } no_data_state = "NoData" @@ -60,7 +60,7 @@ resource "grafana_rule_group" "rule_group_0000" { } datasource_uid = "000000004" - model = "{\n \"alias\": \"just-testing\",\n \"intervalMs\": 1000,\n \"maxDataPoints\": 100,\n \"orgId\": 0,\n \"refId\": \"A\",\n \"scenarioId\": \"csv_metric_values\",\n \"stringInput\": \"1,20,90,30,5,0\"\n }" + model = "{\"alias\":\"just-testing\",\"intervalMs\":1000,\"maxDataPoints\":100,\"orgId\":0,\"refId\":\"A\",\"scenarioId\":\"csv_metric_values\",\"stringInput\":\"1,20,90,30,5,0\"}" } data { ref_id = "B" @@ -71,7 +71,7 @@ resource "grafana_rule_group" "rule_group_0000" { } datasource_uid = "__expr__" - model = "{\n \"expression\": \"$A\",\n \"intervalMs\": 2000,\n \"maxDataPoints\": 200,\n \"orgId\": 0,\n \"reducer\": \"mean\",\n \"refId\": \"B\",\n \"type\": \"reduce\"\n }" + model = "{\"expression\":\"$A\",\"intervalMs\":2000,\"maxDataPoints\":200,\"orgId\":0,\"reducer\":\"mean\",\"refId\":\"B\",\"type\":\"reduce\"}" } no_data_state = "NoData"