Alerting: Add escape and quote support to the matcher name (#83601)

Add escape and quote support to the matcher name
This commit is contained in:
Konrad Lalik 2024-03-01 11:34:58 +01:00 committed by GitHub
parent 36a19bfa83
commit 11d341d2bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 92 additions and 9 deletions

View File

@ -72,10 +72,36 @@ 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\\"\\\\"',
]);
});
it('should quote and escape matcher names', () => {
// Arrange
const route: FormAmRoute = buildFormAmRoute({
id: '1',
object_matchers: [
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'foo with spaces', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'foo\\slash', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'foo"quote', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'fo\\o', operator: MatcherOperator.equal, value: 'ba\\r' },
],
});
// Act
const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' });
// Assert
expect(amRoute.matchers).toStrictEqual([
'"foo"="bar"',
'"foo with spaces"="bar"',
'"foo\\\\slash"="bar"',
'"foo\\"quote"="bar"',
'"fo\\\\o"="ba\\\\r"',
]);
});
@ -90,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', () => {
@ -173,4 +199,23 @@ describe('amRouteToFormAmRoute', () => {
{ name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' },
]);
});
it('should unquote and unescape matcher names', () => {
// Arrange
const amRoute = buildAmRoute({
matchers: ['"foo"=bar', '"foo with spaces"=bar', '"foo\\\\slash"=bar', '"foo"quote"=bar', '"fo\\\\o"="ba\\\\r"'],
});
// Act
const formRoute = amRouteToFormAmRoute(amRoute);
// Assert
expect(formRoute.object_matchers).toStrictEqual([
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'foo with spaces', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'foo\\slash', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'foo"quote', operator: MatcherOperator.equal, value: 'bar' },
{ name: 'fo\\o', operator: MatcherOperator.equal, value: 'ba\\r' },
]);
});
});

View File

@ -98,7 +98,7 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo
route.matchers
?.map((matcher) => matcherToMatcherField(parseMatcher(matcher)))
.map(({ name, operator, value }) => ({
name,
name: unquoteWithUnescape(name),
operator,
value: unquoteWithUnescape(value),
})) ?? [];
@ -186,7 +186,7 @@ export const formAmRouteToAmRoute = (
// does not exist in upstream AlertManager
if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {
amRoute.matchers = formAmRoute.object_matchers?.map(
({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}`
({ name, operator, value }) => `${quoteWithEscape(name)}${operator}${quoteWithEscape(value)}`
);
amRoute.object_matchers = undefined;
} else {

View File

@ -137,9 +137,10 @@ export const matcherFormatter = {
return `${name} ${operator} ${formattedValue}`;
},
unquote: ([name, operator, value]: ObjectMatcher): string => {
const unquotedName = unquoteWithUnescape(name);
// Unquoted value can be an empty string which we want to display as ""
const unquotedValue = unquoteWithUnescape(value) || '""';
return `${name} ${operator} ${unquotedValue}`;
return `${unquotedName} ${operator} ${unquotedValue}`;
},
} as const;

View File

@ -7,6 +7,7 @@ import {
getInheritedProperties,
matchLabels,
normalizeRoute,
unquoteRouteMatchers,
} from './notification-policies';
import 'core-js/stable/structured-clone';
@ -476,3 +477,39 @@ describe('matchLabels', () => {
expect(result.labelsMatch).toMatchSnapshot();
});
});
describe('unquoteRouteMatchers', () => {
it('should unquote and unescape matchers values', () => {
const route: RouteWithID = {
id: '1',
object_matchers: [
['foo', MatcherOperator.equal, 'bar'],
['foo', MatcherOperator.equal, '"bar"'],
['foo', MatcherOperator.equal, '"b\\\\ar b\\"az"'],
],
};
const unwrapped = unquoteRouteMatchers(route);
expect(unwrapped.object_matchers).toHaveLength(3);
expect(unwrapped.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'bar']);
expect(unwrapped.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'bar']);
expect(unwrapped.object_matchers).toContainEqual(['foo', MatcherOperator.equal, 'b\\ar b"az']);
});
it('should unquote and unescape matcher names', () => {
const route: RouteWithID = {
id: '1',
object_matchers: [
['"f\\"oo with quote"', MatcherOperator.equal, 'bar'],
['"f\\\\oo with slash"', MatcherOperator.equal, 'bar'],
],
};
const unwrapped = unquoteRouteMatchers(route);
expect(unwrapped.object_matchers).toHaveLength(2);
expect(unwrapped.object_matchers).toContainEqual(['f"oo with quote', MatcherOperator.equal, 'bar']);
expect(unwrapped.object_matchers).toContainEqual(['f\\oo with slash', MatcherOperator.equal, 'bar']);
});
});

View File

@ -127,7 +127,7 @@ export function normalizeRoute(rootRoute: RouteWithID): RouteWithID {
export function unquoteRouteMatchers(route: RouteWithID): RouteWithID {
function unquoteRoute(route: RouteWithID) {
route.object_matchers = route.object_matchers?.map(([name, operator, value]) => {
return [name, operator, unquoteWithUnescape(value)];
return [unquoteWithUnescape(name), operator, unquoteWithUnescape(value)];
});
route.routes?.forEach(unquoteRoute);
}