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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 48 additions and 10 deletions

View File

@ -72,10 +72,10 @@ describe('formAmRouteToAmRoute', () => {
// Assert
expect(amRoute.matchers).toStrictEqual([
'"foo"="bar"',
'"foo"="bar\\"baz"',
'"foo"="bar\\\\baz"',
'"foo"="\\\\bar\\\\baz\\"\\\\"',
'foo="bar"',
'foo="bar\\"baz"',
'foo="bar\\\\baz"',
'foo="\\\\bar\\\\baz\\"\\\\"',
]);
});
@ -97,7 +97,7 @@ describe('formAmRouteToAmRoute', () => {
// Assert
expect(amRoute.matchers).toStrictEqual([
'"foo"="bar"',
'foo="bar"',
'"foo with spaces"="bar"',
'"foo\\\\slash"="bar"',
'"foo\\"quote"="bar"',
@ -116,7 +116,7 @@ describe('formAmRouteToAmRoute', () => {
const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' });
// Assert
expect(amRoute.matchers).toStrictEqual(['"foo"=""']);
expect(amRoute.matchers).toStrictEqual(['foo=""']);
});
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 { 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 { 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
// does not exist in upstream AlertManager
if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {
amRoute.matchers = formAmRoute.object_matchers?.map(
({ name, operator, value }) => `${quoteWithEscape(name)}${operator}${quoteWithEscape(value)}`
);
// to support UTF-8 characters we must wrap label keys and values with double quotes if they contain reserved characters.
amRoute.matchers = formAmRoute.object_matchers?.map(encodeMatcher);
amRoute.object_matchers = undefined;
} else {
amRoute.object_matchers = normalizeMatchers(amRoute);

View File

@ -1,6 +1,7 @@
import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
import {
encodeMatcher,
getMatcherQueryParams,
isPromQLStyleMatcher,
matcherToObjectMatcher,
@ -9,6 +10,7 @@ import {
parsePromQLStyleMatcher,
parseQueryParamMatchers,
quoteWithEscape,
quoteWithEscapeIfRequired,
unquoteWithUnescape,
} from './matchers';
@ -175,4 +177,19 @@ describe('parsePromQLStyleMatcher', () => {
it('should throw when not using correct syntax', () => {
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 { Labels } from '../../../../types/unified-alerting-dto';
import { MatcherFieldValue } from '../types/silence-form';
import { isPrivateLabelKey } from './labels';
@ -144,6 +145,27 @@ export function quoteWithEscape(input: string) {
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**
*/