mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Apply matchers encoding and decoding on the RTKQ layer
This commit is contained in:
parent
820f4717bf
commit
4d963c43b5
@ -1,3 +1,4 @@
|
||||
import { produce } from 'immer';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { dispatch } from 'app/store/store';
|
||||
@ -15,7 +16,7 @@ import {
|
||||
} from '../../../../plugins/datasource/alertmanager/types';
|
||||
import { NotifierDTO } from '../../../../types';
|
||||
import { withPerformanceLogging } from '../Analytics';
|
||||
import { matcherToOperator } from '../utils/alertmanager';
|
||||
import { matcherToOperator, quoteAmConfigMatchers, unquoteRouteMatchers } from '../utils/alertmanager';
|
||||
import {
|
||||
getDatasourceAPIUid,
|
||||
GRAFANA_RULES_SOURCE_NAME,
|
||||
@ -196,7 +197,11 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
return produce(result, (draft) => {
|
||||
if (draft.alertmanager_config.route) {
|
||||
unquoteRouteMatchers(draft.alertmanager_config.route);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then((result) => result ?? defaultConfig)
|
||||
.then((result) => ({ data: result }))
|
||||
@ -224,12 +229,14 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
||||
void,
|
||||
{ selectedAlertmanager: string; config: AlertManagerCortexConfig }
|
||||
>({
|
||||
query: ({ selectedAlertmanager, config, ...rest }) => ({
|
||||
url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
|
||||
method: 'POST',
|
||||
data: config,
|
||||
...rest,
|
||||
}),
|
||||
query: ({ selectedAlertmanager, config, ...rest }) => {
|
||||
return {
|
||||
url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
|
||||
method: 'POST',
|
||||
data: quoteAmConfigMatchers(config),
|
||||
...rest,
|
||||
};
|
||||
},
|
||||
invalidatesTags: ['AlertmanagerConfiguration'],
|
||||
}),
|
||||
|
||||
|
@ -10,13 +10,15 @@ import { useDispatch } from 'app/types';
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { expireSilenceAction } from '../../state/actions';
|
||||
import { parseMatchers } from '../../utils/alertmanager';
|
||||
import { matcherToObjectMatcher } from '../../utils/matchers';
|
||||
import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { Matchers } from '../notification-policies/Matchers';
|
||||
import { ActionButton } from '../rules/ActionButton';
|
||||
import { ActionIcon } from '../rules/ActionIcon';
|
||||
|
||||
import { Matchers } from './Matchers';
|
||||
// import { Matchers } from './Matchers';
|
||||
import { NoSilencesSplash } from './NoSilencesCTA';
|
||||
import { SilenceDetails } from './SilenceDetails';
|
||||
import { SilenceStateTag } from './SilenceStateTag';
|
||||
@ -219,8 +221,9 @@ function useColumns(alertManagerSourceName: string) {
|
||||
{
|
||||
id: 'matchers',
|
||||
label: 'Matching labels',
|
||||
renderCell: function renderMatchers({ data: { matchers } }) {
|
||||
return <Matchers matchers={matchers || []} />;
|
||||
renderCell: function renderMatchers({ data: { matchers = [] } }) {
|
||||
const objectMatchers = matchers.map(matcherToObjectMatcher);
|
||||
return <Matchers matchers={objectMatchers} />;
|
||||
},
|
||||
size: 10,
|
||||
},
|
||||
|
@ -66,7 +66,11 @@ import {
|
||||
setRulerRuleGroup,
|
||||
} from '../api/ruler';
|
||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
||||
import {
|
||||
addDefaultsToAlertmanagerConfig,
|
||||
quoteAmConfigMatchers,
|
||||
removeMuteTimingFromRoute,
|
||||
} from '../utils/alertmanager';
|
||||
import {
|
||||
getAllRulesSourceNames,
|
||||
getRulesDataSource,
|
||||
@ -530,7 +534,11 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
|
||||
'A newer Alertmanager configuration is available. Please reload the page and try again to not overwrite recent changes.'
|
||||
);
|
||||
}
|
||||
await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig));
|
||||
|
||||
await updateAlertManagerConfig(
|
||||
alertManagerSourceName,
|
||||
addDefaultsToAlertmanagerConfig(quoteAmConfigMatchers(newConfig))
|
||||
);
|
||||
thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
|
||||
if (redirectPath) {
|
||||
const options = new URLSearchParams(redirectSearch ?? '');
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { produce } from 'immer';
|
||||
import { isEqual, uniqWith } from 'lodash';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
@ -16,6 +17,8 @@ import { MatcherFieldValue } from '../types/silence-form';
|
||||
|
||||
import { getAllDataSources } from './config';
|
||||
import { DataSourceType } from './datasource';
|
||||
import { parseMatcher } from './matchers';
|
||||
import { isQuoted, quoteWithEscape, unquoteWithUnescape } from './misc';
|
||||
|
||||
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
|
||||
// add default receiver if it does not exist
|
||||
@ -138,6 +141,39 @@ export function parseMatchers(matcherQueryString: string): Matcher[] {
|
||||
return matchers;
|
||||
}
|
||||
|
||||
export function unquoteRouteMatchers(route: Route): void {
|
||||
if (route.matchers) {
|
||||
route.matchers = route.matchers.map((stringMatcher) => {
|
||||
const [name, operator, value] = matcherToObjectMatcher(parseMatcher(stringMatcher));
|
||||
if (isQuoted(value)) {
|
||||
return `${name}${operator}${unquoteWithUnescape(value)}`;
|
||||
}
|
||||
return stringMatcher;
|
||||
});
|
||||
}
|
||||
route.routes?.forEach(unquoteRouteMatchers);
|
||||
}
|
||||
|
||||
export function quoteRouteMatchers(route: Route): void {
|
||||
if (route.matchers) {
|
||||
route.matchers = route.matchers.map((stringMatcher) => {
|
||||
const matcher = parseMatcher(stringMatcher);
|
||||
const { name, value, operator } = matcherToMatcherField(matcher);
|
||||
return `${name}${operator}${quoteWithEscape(value)}`;
|
||||
});
|
||||
}
|
||||
route.routes?.forEach(quoteRouteMatchers);
|
||||
}
|
||||
|
||||
export function quoteAmConfigMatchers(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
|
||||
return produce(config, (draft) => {
|
||||
if (draft.alertmanager_config.route) {
|
||||
// Grafana AM doesn't use the matchers field so shouldn't be affected
|
||||
quoteRouteMatchers(draft.alertmanager_config.route);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getValidRegexString(regex: string): string {
|
||||
// Regexes provided by users might be invalid, so we need to catch the error
|
||||
try {
|
||||
|
@ -53,30 +53,6 @@ describe('formAmRouteToAmRoute', () => {
|
||||
expect(amRoute.group_by).toStrictEqual(['SHOULD BE SET']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should quote and escape matcher values', () => {
|
||||
// Arrange
|
||||
const route: FormAmRoute = buildFormAmRoute({
|
||||
id: '1',
|
||||
object_matchers: [
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar"baz' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar\\baz' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' },
|
||||
],
|
||||
});
|
||||
|
||||
// Act
|
||||
const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' });
|
||||
|
||||
// Assert
|
||||
expect(amRoute.matchers).toStrictEqual([
|
||||
'foo="bar"',
|
||||
'foo="bar\\"baz"',
|
||||
'foo="bar\\\\baz"',
|
||||
'foo="\\\\bar\\\\baz\\"\\\\"',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('amRouteToFormAmRoute', () => {
|
||||
@ -125,23 +101,4 @@ describe('amRouteToFormAmRoute', () => {
|
||||
expect(formRoute.overrideGrouping).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should unquote and unescape matchers values', () => {
|
||||
// Arrange
|
||||
const amRoute = buildAmRoute({
|
||||
matchers: ['foo=bar', 'foo="bar"', 'foo="bar"baz"', 'foo="bar\\\\baz"', 'foo="\\\\bar\\\\baz"\\\\"'],
|
||||
});
|
||||
|
||||
// Act
|
||||
const formRoute = amRouteToFormAmRoute(amRoute);
|
||||
|
||||
// Assert
|
||||
expect(formRoute.object_matchers).toStrictEqual([
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar"baz' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: 'bar\\baz' },
|
||||
{ name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -9,7 +9,6 @@ import { MatcherFieldValue } from '../types/silence-form';
|
||||
import { matcherToMatcherField } from './alertmanager';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
||||
import { normalizeMatchers, parseMatcher } from './matchers';
|
||||
import { quoteWithEscape, unquoteWithUnescape } from './misc';
|
||||
import { findExistingRoute } from './routeTree';
|
||||
import { isValidPrometheusDuration, safeParseDurationstr } from './time';
|
||||
|
||||
@ -101,7 +100,7 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo
|
||||
.map(({ name, operator, value }) => ({
|
||||
name,
|
||||
operator,
|
||||
value: unquoteWithUnescape(value),
|
||||
value,
|
||||
})) ?? [];
|
||||
|
||||
return {
|
||||
@ -185,9 +184,7 @@ 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 }) => `${name}${operator}${quoteWithEscape(value)}`
|
||||
);
|
||||
amRoute.matchers = formAmRoute.object_matchers?.map(({ name, operator, value }) => `${name}${operator}${value}`);
|
||||
amRoute.object_matchers = undefined;
|
||||
} else {
|
||||
amRoute.object_matchers = normalizeMatchers(amRoute);
|
||||
|
@ -60,6 +60,27 @@ export const getMatcherQueryParams = (labels: Labels) => {
|
||||
return matcherUrlParams;
|
||||
};
|
||||
|
||||
export const matcherToObjectMatcher = (matcher: Matcher): ObjectMatcher => {
|
||||
const { name, value, isRegex, isEqual } = matcher;
|
||||
|
||||
let operator = MatcherOperator.equal;
|
||||
|
||||
if (isEqual && isRegex) {
|
||||
operator = MatcherOperator.regex;
|
||||
}
|
||||
if (!isEqual && isRegex) {
|
||||
operator = MatcherOperator.notRegex;
|
||||
}
|
||||
if (isEqual && !isRegex) {
|
||||
operator = MatcherOperator.equal;
|
||||
}
|
||||
if (!isEqual && !isRegex) {
|
||||
operator = MatcherOperator.notEqual;
|
||||
}
|
||||
|
||||
return [name, operator, value];
|
||||
};
|
||||
|
||||
/**
|
||||
* We need to deal with multiple (deprecated) properties such as "match" and "match_re"
|
||||
* this function will normalize all of the different ways to define matchers in to a single one.
|
||||
@ -68,24 +89,11 @@ export const normalizeMatchers = (route: Route): ObjectMatcher[] => {
|
||||
const matchers: ObjectMatcher[] = [];
|
||||
|
||||
if (route.matchers) {
|
||||
route.matchers.forEach((matcher) => {
|
||||
const { name, value, isEqual, isRegex } = parseMatcher(matcher);
|
||||
let operator = MatcherOperator.equal;
|
||||
route.matchers.forEach((stringMatcher) => {
|
||||
const matcher = parseMatcher(stringMatcher);
|
||||
const objectMatcher = matcherToObjectMatcher(matcher);
|
||||
|
||||
if (isEqual && isRegex) {
|
||||
operator = MatcherOperator.regex;
|
||||
}
|
||||
if (!isEqual && isRegex) {
|
||||
operator = MatcherOperator.notRegex;
|
||||
}
|
||||
if (isEqual && !isRegex) {
|
||||
operator = MatcherOperator.equal;
|
||||
}
|
||||
if (!isEqual && !isRegex) {
|
||||
operator = MatcherOperator.notEqual;
|
||||
}
|
||||
|
||||
matchers.push([name, operator, value]);
|
||||
matchers.push(objectMatcher);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,6 +129,10 @@ export function unquoteWithUnescape(input: string) {
|
||||
.replace(/\\"/g, '"');
|
||||
}
|
||||
|
||||
export function isQuoted(input: string) {
|
||||
return input.startsWith('"') && input.endsWith('"');
|
||||
}
|
||||
|
||||
export function makeRuleBasedSilenceLink(alertManagerSourceName: string, rule: CombinedRule) {
|
||||
// we wrap the name of the alert with quotes since it might contain starting and trailing spaces
|
||||
const labels: Labels = {
|
||||
|
Loading…
Reference in New Issue
Block a user