mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: miscellaneous alertmanager config issues, tests (#35364)
This commit is contained in:
parent
673b03671d
commit
0439009d4f
@ -3,15 +3,16 @@ import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AlertManagerCortexConfig, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { typeAsJestMock } from 'test/helpers/typeAsJestMock';
|
||||
import { byTestId } from 'testing-library-selector';
|
||||
import { byRole, byTestId, byText } from 'testing-library-selector';
|
||||
import AmRoutes from './AmRoutes';
|
||||
import { fetchAlertManagerConfig } from './api/alertmanager';
|
||||
import { fetchAlertManagerConfig, updateAlertManagerConfig } from './api/alertmanager';
|
||||
import { mockDataSource, MockDataSourceSrv } from './mocks';
|
||||
import { getAllDataSources } from './utils/config';
|
||||
import { DataSourceType } from './utils/datasource';
|
||||
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
@ -35,6 +36,7 @@ const mocks = {
|
||||
|
||||
api: {
|
||||
fetchAlertManagerConfig: typeAsJestMock(fetchAlertManagerConfig),
|
||||
updateAlertManagerConfig: typeAsJestMock(updateAlertManagerConfig),
|
||||
},
|
||||
};
|
||||
|
||||
@ -62,6 +64,18 @@ const ui = {
|
||||
rootGroupBy: byTestId('am-routes-root-group-by'),
|
||||
rootTimings: byTestId('am-routes-root-timings'),
|
||||
row: byTestId('am-routes-row'),
|
||||
|
||||
rootRouteContainer: byTestId('am-root-route-container'),
|
||||
|
||||
editButton: byRole('button', { name: 'Edit' }),
|
||||
saveButton: byRole('button', { name: 'Save' }),
|
||||
|
||||
receiverSelect: byTestId('am-receiver-select'),
|
||||
groupSelect: byTestId('am-group-select'),
|
||||
|
||||
groupWaitContainer: byTestId('am-group-wait'),
|
||||
groupIntervalContainer: byTestId('am-group-interval'),
|
||||
groupRepeatContainer: byTestId('am-repeat-interval'),
|
||||
};
|
||||
|
||||
describe('AmRoutes', () => {
|
||||
@ -130,31 +144,8 @@ describe('AmRoutes', () => {
|
||||
routes: subroutes,
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
||||
|
||||
mocks.api.fetchAlertManagerConfig.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
alertmanager_config: {
|
||||
route: rootRoute,
|
||||
receivers: [
|
||||
{
|
||||
name: 'default-receiver',
|
||||
},
|
||||
{
|
||||
name: 'a-receiver',
|
||||
},
|
||||
{
|
||||
name: 'another-receiver',
|
||||
},
|
||||
],
|
||||
},
|
||||
template_files: {},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
|
||||
setDataSourceSrv(new MockDataSourceSrv(dataSources));
|
||||
});
|
||||
|
||||
@ -165,6 +156,24 @@ describe('AmRoutes', () => {
|
||||
});
|
||||
|
||||
it('loads and shows routes', async () => {
|
||||
mocks.api.fetchAlertManagerConfig.mockResolvedValue({
|
||||
alertmanager_config: {
|
||||
route: rootRoute,
|
||||
receivers: [
|
||||
{
|
||||
name: 'default-receiver',
|
||||
},
|
||||
{
|
||||
name: 'a-receiver',
|
||||
},
|
||||
{
|
||||
name: 'another-receiver',
|
||||
},
|
||||
],
|
||||
},
|
||||
template_files: {},
|
||||
});
|
||||
|
||||
await renderAmRoutes();
|
||||
|
||||
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
|
||||
@ -197,4 +206,150 @@ describe('AmRoutes', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('can edit root route if one is already defined', async () => {
|
||||
const defaultConfig: AlertManagerCortexConfig = {
|
||||
alertmanager_config: {
|
||||
receivers: [{ name: 'default' }, { name: 'critical' }],
|
||||
route: {
|
||||
receiver: 'default',
|
||||
group_by: ['alertname'],
|
||||
},
|
||||
templates: [],
|
||||
},
|
||||
template_files: {},
|
||||
};
|
||||
const currentConfig = { current: defaultConfig };
|
||||
mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => {
|
||||
currentConfig.current = newConfig;
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
mocks.api.fetchAlertManagerConfig.mockImplementation(() => {
|
||||
return Promise.resolve(currentConfig.current);
|
||||
});
|
||||
|
||||
await renderAmRoutes();
|
||||
expect(await ui.rootReceiver.find()).toHaveTextContent('default');
|
||||
expect(ui.rootGroupBy.get()).toHaveTextContent('alertname');
|
||||
|
||||
// open root route for editing
|
||||
const rootRouteContainer = await ui.rootRouteContainer.find();
|
||||
userEvent.click(ui.editButton.get(rootRouteContainer));
|
||||
|
||||
// configure receiver & group by
|
||||
const receiverSelect = await ui.receiverSelect.find();
|
||||
clickSelectOption(receiverSelect, 'critical');
|
||||
|
||||
const groupSelect = ui.groupSelect.get();
|
||||
await userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}');
|
||||
|
||||
// configure timing intervals
|
||||
userEvent.click(byText('Timing options').get(rootRouteContainer));
|
||||
|
||||
await updateTiming(ui.groupWaitContainer.get(), '1', 'Minutes');
|
||||
await updateTiming(ui.groupIntervalContainer.get(), '4', 'Minutes');
|
||||
await updateTiming(ui.groupRepeatContainer.get(), '5', 'Hours');
|
||||
|
||||
//save
|
||||
userEvent.click(ui.saveButton.get(rootRouteContainer));
|
||||
|
||||
// wait for it to go out of edit mode
|
||||
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
|
||||
|
||||
// check that appropriate api calls were made
|
||||
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
alertmanager_config: {
|
||||
receivers: [{ name: 'default' }, { name: 'critical' }],
|
||||
route: {
|
||||
continue: false,
|
||||
group_by: ['alertname', 'namespace'],
|
||||
receiver: 'critical',
|
||||
routes: [],
|
||||
group_interval: '4m',
|
||||
group_wait: '1m',
|
||||
repeat_interval: '5h',
|
||||
},
|
||||
templates: [],
|
||||
},
|
||||
template_files: {},
|
||||
});
|
||||
|
||||
// check that new config values are rendered
|
||||
await waitFor(() => expect(ui.rootReceiver.query()).toHaveTextContent('critical'));
|
||||
expect(ui.rootGroupBy.get()).toHaveTextContent('alertname, namespace');
|
||||
});
|
||||
|
||||
it('can edit root route if one is not defined yet', async () => {
|
||||
mocks.api.fetchAlertManagerConfig.mockResolvedValue({
|
||||
alertmanager_config: {
|
||||
receivers: [{ name: 'default' }],
|
||||
},
|
||||
template_files: {},
|
||||
});
|
||||
|
||||
await renderAmRoutes();
|
||||
|
||||
// open root route for editing
|
||||
const rootRouteContainer = await ui.rootRouteContainer.find();
|
||||
userEvent.click(ui.editButton.get(rootRouteContainer));
|
||||
|
||||
// configure receiver & group by
|
||||
const receiverSelect = await ui.receiverSelect.find();
|
||||
clickSelectOption(receiverSelect, 'default');
|
||||
|
||||
const groupSelect = ui.groupSelect.get();
|
||||
await userEvent.type(byRole('textbox').get(groupSelect), 'severity{enter}');
|
||||
await userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}');
|
||||
//save
|
||||
userEvent.click(ui.saveButton.get(rootRouteContainer));
|
||||
|
||||
// wait for it to go out of edit mode
|
||||
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
|
||||
|
||||
// check that appropriate api calls were made
|
||||
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
|
||||
alertmanager_config: {
|
||||
receivers: [{ name: 'default' }],
|
||||
route: {
|
||||
continue: false,
|
||||
group_by: ['severity', 'namespace'],
|
||||
receiver: 'default',
|
||||
routes: [],
|
||||
},
|
||||
},
|
||||
template_files: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('Show error message if loading Alermanager config fails', async () => {
|
||||
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
|
||||
status: 500,
|
||||
data: {
|
||||
message: "Alertmanager has exploded. it's gone. Forget about it.",
|
||||
},
|
||||
});
|
||||
await renderAmRoutes();
|
||||
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
|
||||
expect(await byText("Alertmanager has exploded. it's gone. Forget about it.").find()).toBeInTheDocument();
|
||||
expect(ui.rootReceiver.query()).not.toBeInTheDocument();
|
||||
expect(ui.editButton.query()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
|
||||
userEvent.click(byRole('textbox').get(selectElement));
|
||||
userEvent.click(byText(optionText).get(selectElement));
|
||||
};
|
||||
|
||||
const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit: string): Promise<void> => {
|
||||
const inputs = byRole('textbox').queryAll(selectElement);
|
||||
expect(inputs).toHaveLength(2);
|
||||
await userEvent.type(inputs[0], value);
|
||||
userEvent.click(inputs[1]);
|
||||
userEvent.click(byText(timeUnit).get(selectElement));
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ const AmRoutes: FC = () => {
|
||||
(alertManagerSourceName && amConfigs[alertManagerSourceName]) || initialAsyncRequestState;
|
||||
|
||||
const config = result?.alertmanager_config;
|
||||
const [routes, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
|
||||
const [rootRoute, id2ExistingRoute] = useMemo(() => amRouteToFormAmRoute(config?.route), [config?.route]);
|
||||
|
||||
const receivers = stringsToSelectableValues(
|
||||
(config?.receivers ?? []).map((receiver: Receiver) => receiver.name)
|
||||
@ -54,14 +54,10 @@ const AmRoutes: FC = () => {
|
||||
};
|
||||
|
||||
useCleanup((state) => state.unifiedAlerting.saveAMConfig);
|
||||
const { loading: saving, error: savingError, dispatched: savingDispatched } = useUnifiedAlertingSelector(
|
||||
(state) => state.saveAMConfig
|
||||
);
|
||||
|
||||
const handleSave = (data: Partial<FormAmRoute>) => {
|
||||
const newData = formAmRouteToAmRoute(
|
||||
{
|
||||
...routes,
|
||||
...rootRoute,
|
||||
...data,
|
||||
},
|
||||
id2ExistingRoute
|
||||
@ -83,16 +79,11 @@ const AmRoutes: FC = () => {
|
||||
oldConfig: result,
|
||||
alertManagerSourceName: alertManagerSourceName!,
|
||||
successMessage: 'Saved',
|
||||
refetch: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (savingDispatched && !saving && !savingError) {
|
||||
fetchConfig();
|
||||
}
|
||||
}, [fetchConfig, savingDispatched, saving, savingError]);
|
||||
|
||||
if (!alertManagerSourceName) {
|
||||
return <Redirect to="/alerting/routes" />;
|
||||
}
|
||||
@ -115,14 +106,14 @@ const AmRoutes: FC = () => {
|
||||
onEnterEditMode={enterRootRouteEditMode}
|
||||
onExitEditMode={exitRootRouteEditMode}
|
||||
receivers={receivers}
|
||||
routes={routes}
|
||||
routes={rootRoute}
|
||||
/>
|
||||
<div className={styles.break} />
|
||||
<AmSpecificRouting
|
||||
onChange={handleSave}
|
||||
onRootRouteEdit={enterRootRouteEditMode}
|
||||
receivers={receivers}
|
||||
routes={routes}
|
||||
routes={rootRoute}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -28,8 +28,7 @@ export async function fetchAlertManagerConfig(alertManagerSourceName: string): P
|
||||
// if no config has been uploaded to grafana, it returns error instead of latest config
|
||||
if (
|
||||
alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME &&
|
||||
(e.data?.message?.includes('failed to get latest configuration') ||
|
||||
e.data?.message?.includes('could not find an Alertmanager configuration'))
|
||||
e.data?.message?.includes('could not find an Alertmanager configuration')
|
||||
) {
|
||||
return {
|
||||
template_files: {},
|
||||
|
@ -28,7 +28,7 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} data-testid="am-root-route-container">
|
||||
<div className={styles.titleContainer}>
|
||||
<h5 className={styles.title}>
|
||||
Root policy - <i>default for all alerts</i>
|
||||
|
@ -36,27 +36,34 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
||||
<Form defaultValues={routes} onSubmit={onSave}>
|
||||
{({ control, errors, setValue }) => (
|
||||
<>
|
||||
<Field label="Default contact point">
|
||||
<div className={styles.container}>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Select
|
||||
{...field}
|
||||
className={styles.input}
|
||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||
options={receivers}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
name="receiver"
|
||||
/>
|
||||
<span>or</span>
|
||||
<Link href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}>
|
||||
Create a contact point
|
||||
</Link>
|
||||
</div>
|
||||
<Field label="Default contact point" invalid={!!errors.receiver} error={errors.receiver?.message}>
|
||||
<>
|
||||
<div className={styles.container} data-testid="am-receiver-select">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<Select
|
||||
{...field}
|
||||
className={styles.input}
|
||||
onChange={(value) => onChange(mapSelectValueToString(value))}
|
||||
options={receivers}
|
||||
/>
|
||||
)}
|
||||
control={control}
|
||||
name="receiver"
|
||||
rules={{ required: { value: true, message: 'Required.' } }}
|
||||
/>
|
||||
<span>or</span>
|
||||
<Link href={makeAMLink('/alerting/notifications/receivers/new', alertManagerSourceName)}>
|
||||
Create a contact point
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
</Field>
|
||||
<Field label="Group by" description="Group alerts when you receive a notification based on labels.">
|
||||
<Field
|
||||
label="Group by"
|
||||
description="Group alerts when you receive a notification based on labels."
|
||||
data-testid="am-group-select"
|
||||
>
|
||||
{/* @ts-ignore-check: react-hook-form made me do this */}
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
@ -86,9 +93,10 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
||||
>
|
||||
<Field
|
||||
label="Group wait"
|
||||
description="The waiting time until the initial notification is sent for a new group created by an incoming alert."
|
||||
description="The waiting time until the initial notification is sent for a new group created by an incoming alert. Default 30 seconds."
|
||||
invalid={!!errors.groupWaitValue}
|
||||
error={errors.groupWaitValue?.message}
|
||||
data-testid="am-group-wait"
|
||||
>
|
||||
<>
|
||||
<div className={cx(styles.container, styles.timingContainer)}>
|
||||
@ -119,9 +127,10 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
||||
</Field>
|
||||
<Field
|
||||
label="Group interval"
|
||||
description="The waiting time to send a batch of new alerts for that group after the first notification was sent."
|
||||
description="The waiting time to send a batch of new alerts for that group after the first notification was sent. Default 5 minutes."
|
||||
invalid={!!errors.groupIntervalValue}
|
||||
error={errors.groupIntervalValue?.message}
|
||||
data-testid="am-group-interval"
|
||||
>
|
||||
<>
|
||||
<div className={cx(styles.container, styles.timingContainer)}>
|
||||
@ -152,9 +161,10 @@ export const AmRootRouteForm: FC<AmRootRouteFormProps> = ({
|
||||
</Field>
|
||||
<Field
|
||||
label="Repeat interval"
|
||||
description="The waiting time to resend an alert after they have successfully been sent."
|
||||
description="The waiting time to resend an alert after they have successfully been sent. Default 4 hours."
|
||||
invalid={!!errors.repeatIntervalValue}
|
||||
error={errors.repeatIntervalValue?.message}
|
||||
data-testid="am-repeat-interval"
|
||||
>
|
||||
<>
|
||||
<div className={cx(styles.container, styles.timingContainer)}>
|
||||
|
@ -3,7 +3,7 @@ import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
|
||||
import { emptyRoute } from '../../utils/amroutes';
|
||||
import { emptyArrayFieldMatcher, emptyRoute } from '../../utils/amroutes';
|
||||
import { EmptyArea } from '../EmptyArea';
|
||||
import { AmRoutesTable } from './AmRoutesTable';
|
||||
|
||||
@ -22,7 +22,13 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({ onChange, onRoot
|
||||
|
||||
const addNewRoute = () => {
|
||||
setIsAddMode(true);
|
||||
setActualRoutes((actualRoutes) => [...actualRoutes, emptyRoute]);
|
||||
setActualRoutes((actualRoutes) => [
|
||||
...actualRoutes,
|
||||
{
|
||||
...emptyRoute,
|
||||
matchers: [emptyArrayFieldMatcher],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -4,6 +4,7 @@ import { Matcher, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { FormAmRoute } from '../types/amroutes';
|
||||
import { parseInterval, timeOptions } from './time';
|
||||
import { parseMatcher, stringifyMatcher } from './alertmanager';
|
||||
import { isUndefined, omitBy } from 'lodash';
|
||||
|
||||
const defaultValueAndType: [string, string] = ['', timeOptions[0].value];
|
||||
|
||||
@ -51,8 +52,8 @@ export const emptyArrayFieldMatcher: Matcher = {
|
||||
|
||||
export const emptyRoute: FormAmRoute = {
|
||||
id: '',
|
||||
matchers: [emptyArrayFieldMatcher],
|
||||
groupBy: [],
|
||||
matchers: [],
|
||||
routes: [],
|
||||
continue: false,
|
||||
receiver: '',
|
||||
@ -133,7 +134,7 @@ export const formAmRouteToAmRoute = (formAmRoute: FormAmRoute, id2ExistingRoute:
|
||||
amRoute.receiver = formAmRoute.receiver;
|
||||
}
|
||||
|
||||
return amRoute;
|
||||
return omitBy(amRoute, isUndefined);
|
||||
};
|
||||
|
||||
export const stringToSelectableValue = (str: string): SelectableValue<string> => ({
|
||||
|
@ -143,6 +143,8 @@ function messageFromError(e: Error | FetchError | SerializedError): string {
|
||||
.map((d) => d?.message)
|
||||
.filter((m) => !!m)
|
||||
.join(' ');
|
||||
} else if (e.statusText) {
|
||||
return e.statusText;
|
||||
}
|
||||
}
|
||||
return (e as Error)?.message || String(e);
|
||||
|
Loading…
Reference in New Issue
Block a user