Alerting: Support utf8_strict_mode: false in Mimir (#90092)

This commit is contained in:
Gilles De Mey
2024-07-05 17:17:45 +02:00
committed by GitHub
parent f88bf474bd
commit 650616a404
4 changed files with 48 additions and 10 deletions

View File

@@ -72,10 +72,10 @@ describe('formAmRouteToAmRoute', () => {
// Assert // Assert
expect(amRoute.matchers).toStrictEqual([ expect(amRoute.matchers).toStrictEqual([
'"foo"="bar"', 'foo="bar"',
'"foo"="bar\\"baz"', 'foo="bar\\"baz"',
'"foo"="bar\\\\baz"', 'foo="bar\\\\baz"',
'"foo"="\\\\bar\\\\baz\\"\\\\"', 'foo="\\\\bar\\\\baz\\"\\\\"',
]); ]);
}); });
@@ -97,7 +97,7 @@ describe('formAmRouteToAmRoute', () => {
// Assert // Assert
expect(amRoute.matchers).toStrictEqual([ expect(amRoute.matchers).toStrictEqual([
'"foo"="bar"', 'foo="bar"',
'"foo with spaces"="bar"', '"foo with spaces"="bar"',
'"foo\\\\slash"="bar"', '"foo\\\\slash"="bar"',
'"foo\\"quote"="bar"', '"foo\\"quote"="bar"',
@@ -116,7 +116,7 @@ describe('formAmRouteToAmRoute', () => {
const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' }); const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' });
// Assert // Assert
expect(amRoute.matchers).toStrictEqual(['"foo"=""']); expect(amRoute.matchers).toStrictEqual(['foo=""']);
}); });
it('should allow matchers with empty values for Grafana AM', () => { it('should allow matchers with empty values for Grafana AM', () => {

View File

@@ -8,7 +8,7 @@ import { MatcherFieldValue } from '../types/silence-form';
import { matcherToMatcherField } from './alertmanager'; import { matcherToMatcherField } from './alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
import { normalizeMatchers, parseMatcherToArray, quoteWithEscape, unquoteWithUnescape } from './matchers'; import { encodeMatcher, normalizeMatchers, parseMatcherToArray, unquoteWithUnescape } from './matchers';
import { findExistingRoute } from './routeTree'; import { findExistingRoute } from './routeTree';
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time'; import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
@@ -189,9 +189,8 @@ export const formAmRouteToAmRoute = (
// Grafana maintains a fork of AM to support all utf-8 characters in the "object_matchers" property values but this // Grafana maintains a fork of AM to support all utf-8 characters in the "object_matchers" property values but this
// does not exist in upstream AlertManager // does not exist in upstream AlertManager
if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) { if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {
amRoute.matchers = formAmRoute.object_matchers?.map( // to support UTF-8 characters we must wrap label keys and values with double quotes if they contain reserved characters.
({ name, operator, value }) => `${quoteWithEscape(name)}${operator}${quoteWithEscape(value)}` amRoute.matchers = formAmRoute.object_matchers?.map(encodeMatcher);
);
amRoute.object_matchers = undefined; amRoute.object_matchers = undefined;
} else { } else {
amRoute.object_matchers = normalizeMatchers(amRoute); amRoute.object_matchers = normalizeMatchers(amRoute);

View File

@@ -1,6 +1,7 @@
import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types'; import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
import { import {
encodeMatcher,
getMatcherQueryParams, getMatcherQueryParams,
isPromQLStyleMatcher, isPromQLStyleMatcher,
matcherToObjectMatcher, matcherToObjectMatcher,
@@ -9,6 +10,7 @@ import {
parsePromQLStyleMatcher, parsePromQLStyleMatcher,
parseQueryParamMatchers, parseQueryParamMatchers,
quoteWithEscape, quoteWithEscape,
quoteWithEscapeIfRequired,
unquoteWithUnescape, unquoteWithUnescape,
} from './matchers'; } from './matchers';
@@ -175,4 +177,19 @@ describe('parsePromQLStyleMatcher', () => {
it('should throw when not using correct syntax', () => { it('should throw when not using correct syntax', () => {
expect(() => parsePromQLStyleMatcher('foo="bar"')).toThrow(); expect(() => parsePromQLStyleMatcher('foo="bar"')).toThrow();
}); });
it('should only encode matchers if the label key contains reserved characters', () => {
expect(quoteWithEscapeIfRequired('foo')).toBe('foo');
expect(quoteWithEscapeIfRequired('foo bar')).toBe('"foo bar"');
expect(quoteWithEscapeIfRequired('foo{}bar')).toBe('"foo{}bar"');
expect(quoteWithEscapeIfRequired('foo\\bar')).toBe('"foo\\\\bar"');
});
it('should properly encode a matcher field', () => {
expect(encodeMatcher({ name: 'foo', operator: MatcherOperator.equal, value: 'baz' })).toBe('foo="baz"');
expect(encodeMatcher({ name: 'foo bar', operator: MatcherOperator.equal, value: 'baz' })).toBe('"foo bar"="baz"');
expect(encodeMatcher({ name: 'foo{}bar', operator: MatcherOperator.equal, value: 'baz qux' })).toBe(
'"foo{}bar"="baz qux"'
);
});
}); });

View File

@@ -10,6 +10,7 @@ import { compact, uniqBy } from 'lodash';
import { Matcher, MatcherOperator, ObjectMatcher, Route } from 'app/plugins/datasource/alertmanager/types'; import { Matcher, MatcherOperator, ObjectMatcher, Route } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from '../../../../types/unified-alerting-dto'; import { Labels } from '../../../../types/unified-alerting-dto';
import { MatcherFieldValue } from '../types/silence-form';
import { isPrivateLabelKey } from './labels'; import { isPrivateLabelKey } from './labels';
@@ -144,6 +145,27 @@ export function quoteWithEscape(input: string) {
return `"${escaped}"`; return `"${escaped}"`;
} }
// The list of reserved characters that indicate we should be escaping the label key / value are
// { } ! = ~ , \ " ' ` and any whitespace (\s), encoded in the regular expression below
//
// See Alertmanager PR: https://github.com/prometheus/alertmanager/pull/3453
const RESERVED_CHARACTERS = /[\{\}\!\=\~\,\\\"\'\`\s]+/;
/**
* Quotes string only when reserved characters are used
*/
export function quoteWithEscapeIfRequired(input: string) {
const shouldQuote = RESERVED_CHARACTERS.test(input);
return shouldQuote ? quoteWithEscape(input) : input;
}
export const encodeMatcher = ({ name, operator, value }: MatcherFieldValue) => {
const encodedLabelName = quoteWithEscapeIfRequired(name);
const encodedLabelValue = quoteWithEscape(value);
return `${encodedLabelName}${operator}${encodedLabelValue}`;
};
/** /**
* Unquotes and unescapes a string **if it has been quoted** * Unquotes and unescapes a string **if it has been quoted**
*/ */