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 { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
@ -15,7 +16,7 @@ import {
|
|||||||
} from '../../../../plugins/datasource/alertmanager/types';
|
} from '../../../../plugins/datasource/alertmanager/types';
|
||||||
import { NotifierDTO } from '../../../../types';
|
import { NotifierDTO } from '../../../../types';
|
||||||
import { withPerformanceLogging } from '../Analytics';
|
import { withPerformanceLogging } from '../Analytics';
|
||||||
import { matcherToOperator } from '../utils/alertmanager';
|
import { matcherToOperator, quoteAmConfigMatchers, unquoteRouteMatchers } from '../utils/alertmanager';
|
||||||
import {
|
import {
|
||||||
getDatasourceAPIUid,
|
getDatasourceAPIUid,
|
||||||
GRAFANA_RULES_SOURCE_NAME,
|
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) => result ?? defaultConfig)
|
||||||
.then((result) => ({ data: result }))
|
.then((result) => ({ data: result }))
|
||||||
@ -224,12 +229,14 @@ export const alertmanagerApi = alertingApi.injectEndpoints({
|
|||||||
void,
|
void,
|
||||||
{ selectedAlertmanager: string; config: AlertManagerCortexConfig }
|
{ selectedAlertmanager: string; config: AlertManagerCortexConfig }
|
||||||
>({
|
>({
|
||||||
query: ({ selectedAlertmanager, config, ...rest }) => ({
|
query: ({ selectedAlertmanager, config, ...rest }) => {
|
||||||
url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
|
return {
|
||||||
method: 'POST',
|
url: `/api/alertmanager/${getDatasourceAPIUid(selectedAlertmanager)}/config/api/v1/alerts`,
|
||||||
data: config,
|
method: 'POST',
|
||||||
...rest,
|
data: quoteAmConfigMatchers(config),
|
||||||
}),
|
...rest,
|
||||||
|
};
|
||||||
|
},
|
||||||
invalidatesTags: ['AlertmanagerConfiguration'],
|
invalidatesTags: ['AlertmanagerConfiguration'],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -10,13 +10,15 @@ import { useDispatch } from 'app/types';
|
|||||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||||
import { expireSilenceAction } from '../../state/actions';
|
import { expireSilenceAction } from '../../state/actions';
|
||||||
import { parseMatchers } from '../../utils/alertmanager';
|
import { parseMatchers } from '../../utils/alertmanager';
|
||||||
|
import { matcherToObjectMatcher } from '../../utils/matchers';
|
||||||
import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
|
import { getSilenceFiltersFromUrlParams, makeAMLink } from '../../utils/misc';
|
||||||
import { Authorize } from '../Authorize';
|
import { Authorize } from '../Authorize';
|
||||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||||
|
import { Matchers } from '../notification-policies/Matchers';
|
||||||
import { ActionButton } from '../rules/ActionButton';
|
import { ActionButton } from '../rules/ActionButton';
|
||||||
import { ActionIcon } from '../rules/ActionIcon';
|
import { ActionIcon } from '../rules/ActionIcon';
|
||||||
|
|
||||||
import { Matchers } from './Matchers';
|
// import { Matchers } from './Matchers';
|
||||||
import { NoSilencesSplash } from './NoSilencesCTA';
|
import { NoSilencesSplash } from './NoSilencesCTA';
|
||||||
import { SilenceDetails } from './SilenceDetails';
|
import { SilenceDetails } from './SilenceDetails';
|
||||||
import { SilenceStateTag } from './SilenceStateTag';
|
import { SilenceStateTag } from './SilenceStateTag';
|
||||||
@ -219,8 +221,9 @@ function useColumns(alertManagerSourceName: string) {
|
|||||||
{
|
{
|
||||||
id: 'matchers',
|
id: 'matchers',
|
||||||
label: 'Matching labels',
|
label: 'Matching labels',
|
||||||
renderCell: function renderMatchers({ data: { matchers } }) {
|
renderCell: function renderMatchers({ data: { matchers = [] } }) {
|
||||||
return <Matchers matchers={matchers || []} />;
|
const objectMatchers = matchers.map(matcherToObjectMatcher);
|
||||||
|
return <Matchers matchers={objectMatchers} />;
|
||||||
},
|
},
|
||||||
size: 10,
|
size: 10,
|
||||||
},
|
},
|
||||||
|
@ -66,7 +66,11 @@ import {
|
|||||||
setRulerRuleGroup,
|
setRulerRuleGroup,
|
||||||
} from '../api/ruler';
|
} from '../api/ruler';
|
||||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
import { RuleFormType, RuleFormValues } from '../types/rule-form';
|
||||||
import { addDefaultsToAlertmanagerConfig, removeMuteTimingFromRoute } from '../utils/alertmanager';
|
import {
|
||||||
|
addDefaultsToAlertmanagerConfig,
|
||||||
|
quoteAmConfigMatchers,
|
||||||
|
removeMuteTimingFromRoute,
|
||||||
|
} from '../utils/alertmanager';
|
||||||
import {
|
import {
|
||||||
getAllRulesSourceNames,
|
getAllRulesSourceNames,
|
||||||
getRulesDataSource,
|
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.'
|
'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']));
|
thunkAPI.dispatch(alertmanagerApi.util.invalidateTags(['AlertmanagerConfiguration']));
|
||||||
if (redirectPath) {
|
if (redirectPath) {
|
||||||
const options = new URLSearchParams(redirectSearch ?? '');
|
const options = new URLSearchParams(redirectSearch ?? '');
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { produce } from 'immer';
|
||||||
import { isEqual, uniqWith } from 'lodash';
|
import { isEqual, uniqWith } from 'lodash';
|
||||||
|
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
@ -16,6 +17,8 @@ import { MatcherFieldValue } from '../types/silence-form';
|
|||||||
|
|
||||||
import { getAllDataSources } from './config';
|
import { getAllDataSources } from './config';
|
||||||
import { DataSourceType } from './datasource';
|
import { DataSourceType } from './datasource';
|
||||||
|
import { parseMatcher } from './matchers';
|
||||||
|
import { isQuoted, quoteWithEscape, unquoteWithUnescape } from './misc';
|
||||||
|
|
||||||
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
|
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
|
||||||
// add default receiver if it does not exist
|
// add default receiver if it does not exist
|
||||||
@ -138,6 +141,39 @@ export function parseMatchers(matcherQueryString: string): Matcher[] {
|
|||||||
return matchers;
|
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 {
|
function getValidRegexString(regex: string): string {
|
||||||
// Regexes provided by users might be invalid, so we need to catch the error
|
// Regexes provided by users might be invalid, so we need to catch the error
|
||||||
try {
|
try {
|
||||||
|
@ -53,30 +53,6 @@ describe('formAmRouteToAmRoute', () => {
|
|||||||
expect(amRoute.group_by).toStrictEqual(['SHOULD BE SET']);
|
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', () => {
|
describe('amRouteToFormAmRoute', () => {
|
||||||
@ -125,23 +101,4 @@ describe('amRouteToFormAmRoute', () => {
|
|||||||
expect(formRoute.overrideGrouping).toBe(true);
|
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 { matcherToMatcherField } from './alertmanager';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
|
||||||
import { normalizeMatchers, parseMatcher } from './matchers';
|
import { normalizeMatchers, parseMatcher } from './matchers';
|
||||||
import { quoteWithEscape, unquoteWithUnescape } from './misc';
|
|
||||||
import { findExistingRoute } from './routeTree';
|
import { findExistingRoute } from './routeTree';
|
||||||
import { isValidPrometheusDuration, safeParseDurationstr } from './time';
|
import { isValidPrometheusDuration, safeParseDurationstr } from './time';
|
||||||
|
|
||||||
@ -101,7 +100,7 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo
|
|||||||
.map(({ name, operator, value }) => ({
|
.map(({ name, operator, value }) => ({
|
||||||
name,
|
name,
|
||||||
operator,
|
operator,
|
||||||
value: unquoteWithUnescape(value),
|
value,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
return {
|
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
|
// 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(
|
amRoute.matchers = formAmRoute.object_matchers?.map(({ name, operator, value }) => `${name}${operator}${value}`);
|
||||||
({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}`
|
|
||||||
);
|
|
||||||
amRoute.object_matchers = undefined;
|
amRoute.object_matchers = undefined;
|
||||||
} else {
|
} else {
|
||||||
amRoute.object_matchers = normalizeMatchers(amRoute);
|
amRoute.object_matchers = normalizeMatchers(amRoute);
|
||||||
|
@ -60,6 +60,27 @@ export const getMatcherQueryParams = (labels: Labels) => {
|
|||||||
return matcherUrlParams;
|
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"
|
* 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.
|
* 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[] = [];
|
const matchers: ObjectMatcher[] = [];
|
||||||
|
|
||||||
if (route.matchers) {
|
if (route.matchers) {
|
||||||
route.matchers.forEach((matcher) => {
|
route.matchers.forEach((stringMatcher) => {
|
||||||
const { name, value, isEqual, isRegex } = parseMatcher(matcher);
|
const matcher = parseMatcher(stringMatcher);
|
||||||
let operator = MatcherOperator.equal;
|
const objectMatcher = matcherToObjectMatcher(matcher);
|
||||||
|
|
||||||
if (isEqual && isRegex) {
|
matchers.push(objectMatcher);
|
||||||
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]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,6 +129,10 @@ export function unquoteWithUnescape(input: string) {
|
|||||||
.replace(/\\"/g, '"');
|
.replace(/\\"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isQuoted(input: string) {
|
||||||
|
return input.startsWith('"') && input.endsWith('"');
|
||||||
|
}
|
||||||
|
|
||||||
export function makeRuleBasedSilenceLink(alertManagerSourceName: string, rule: CombinedRule) {
|
export function makeRuleBasedSilenceLink(alertManagerSourceName: string, rule: CombinedRule) {
|
||||||
// we wrap the name of the alert with quotes since it might contain starting and trailing spaces
|
// we wrap the name of the alert with quotes since it might contain starting and trailing spaces
|
||||||
const labels: Labels = {
|
const labels: Labels = {
|
||||||
|
Loading…
Reference in New Issue
Block a user