Alerting: Fix notification policies tests and implement "stateful" mock endpoints (#94732)

This commit is contained in:
Tom Ratcliffe 2024-10-17 11:57:57 +01:00 committed by GitHub
parent a46ff09bf9
commit 4c5483ee15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 635 additions and 730 deletions

View File

@ -7,10 +7,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector';
import { config } from '@grafana/runtime';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import {
setAlertmanagerConfig,
setGrafanaAlertmanagerConfig,
} from 'app/features/alerting/unified/mocks/server/configure';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID } from 'app/features/alerting/unified/mocks/server/handlers/datasources';
import {
@ -206,8 +203,12 @@ describe('Mute timings', () => {
// FIXME: scope down
grantUserPermissions(Object.values(AccessControlAction));
setGrafanaAlertmanagerConfig(defaultConfig);
setAlertmanagerConfig(defaultConfig);
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
// TODO: Add this at a higher level to ensure that no tests depend on others running first
// Without this, the selected alertmanager in a previous test can affect the next, meaning tests
// pass/fail depending on the order they are run/if they are focused
window.localStorage.clear();
});
it('creates a new mute timing, with mute_time_intervals in config', async () => {
@ -238,9 +239,9 @@ describe('Mute timings', () => {
it('creates a new mute timing, with time_intervals in config', async () => {
const capture = captureRequests();
setAlertmanagerConfig(defaultConfigWithNewTimeIntervalsField);
setAlertmanagerConfig(dataSources.am.uid, defaultConfigWithNewTimeIntervalsField);
renderMuteTimings(<NewMuteTimingPage />, {
search: `?alertmanager=${alertmanagerName}`,
search: `?alertmanager=${dataSources.am.name}`,
});
await fillOutForm({
@ -262,9 +263,9 @@ describe('Mute timings', () => {
});
it('creates a new mute timing, with time_intervals and mute_time_intervals in config', async () => {
setGrafanaAlertmanagerConfig(defaultConfigWithBothTimeIntervalsField);
setAlertmanagerConfig(dataSources.am.uid, defaultConfigWithBothTimeIntervalsField);
renderMuteTimings(<NewMuteTimingPage />, {
search: `?alertmanager=${alertmanagerName}`,
search: `?alertmanager=${dataSources.am.name}`,
});
expect(ui.nameField.get()).toBeInTheDocument();

View File

@ -1,64 +1,85 @@
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { render, userEvent, waitFor, within } from 'test/test-utils';
import 'core-js/stable/structured-clone';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
import { render, screen, userEvent } from 'test/test-utils';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import {
getErrorResponse,
makeAllAlertmanagerConfigFetchFail,
} from 'app/features/alerting/unified/mocks/server/configure';
import {
getAlertmanagerConfig,
setAlertmanagerConfig,
setAlertmanagerStatus,
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import {
TIME_INTERVAL_NAME_FILE_PROVISIONED,
TIME_INTERVAL_NAME_HAPPY_PATH,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import {
AlertManagerCortexConfig,
AlertManagerDataSourceJsonData,
AlertManagerImplementation,
MatcherOperator,
MuteTimeInterval,
Route,
RouteWithID,
} from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import NotificationPolicies, { findRoutesMatchingFilters } from './NotificationPolicies';
import { fetchAlertManagerConfig, fetchStatus, updateAlertManagerConfig } from './api/alertmanager';
import { alertmanagerApi } from './api/alertmanagerApi';
import { discoverAlertmanagerFeatures } from './api/buildInfo';
import { MockDataSourceSrv, mockDataSource, someCloudAlertManagerConfig, someCloudAlertManagerStatus } from './mocks';
import { defaultGroupBy } from './utils/amroutes';
import { getAllDataSources } from './utils/config';
import {
grantUserPermissions,
mockDataSource,
someCloudAlertManagerConfig,
someCloudAlertManagerStatus,
} from './mocks';
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import 'core-js/stable/structured-clone';
jest.mock('./api/alertmanager');
jest.mock('./utils/config');
jest.mock('app/core/services/context_srv');
jest.mock('./api/buildInfo');
jest.mock('./useRouteGroupsMatcher');
const mocks = {
getAllDataSourcesMock: jest.mocked(getAllDataSources),
api: {
fetchAlertManagerConfig: jest.mocked(fetchAlertManagerConfig),
updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig),
fetchStatus: jest.mocked(fetchStatus),
discoverAlertmanagerFeatures: jest.mocked(discoverAlertmanagerFeatures),
},
contextSrv: jest.mocked(contextSrv),
};
setupMswServer();
const renderNotificationPolicies = (alertManagerSourceName?: string) => {
return render(<NotificationPolicies />, {
historyOptions: {
initialEntries: [
'/alerting/routes' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : ''),
],
},
});
const updateTiming = async (selectElement: HTMLElement, value: string): Promise<void> => {
const user = userEvent.setup();
const input = byRole('textbox').get(selectElement);
await user.clear(input);
await user.type(input, value);
};
const openDefaultPolicyEditModal = async () => {
const user = userEvent.setup();
await user.click(await ui.moreActionsDefaultPolicy.find());
await user.click(await ui.editButton.find());
};
const openEditModal = async (
/** (zero-based) Index of the policy in the list to open the edit modal for */
index: number
) => {
const user = userEvent.setup();
await user.click((await ui.moreActions.findAll())[index]);
await user.click(await ui.editButton.find());
};
const renderNotificationPolicies = (alertManagerSourceName: string = GRAFANA_RULES_SOURCE_NAME) =>
render(
<>
<AppNotificationList />
<NotificationPolicies />
</>,
{
historyOptions: {
initialEntries: [
'/alerting/routes' +
(alertManagerSourceName ? `?${ALERTMANAGER_NAME_QUERY_KEY}=${alertManagerSourceName}` : ''),
],
},
}
);
const dataSources = {
am: mockDataSource({
name: 'Alertmanager',
@ -67,30 +88,36 @@ const dataSources = {
promAlertManager: mockDataSource<AlertManagerDataSourceJsonData>({
name: 'PromManager',
type: DataSourceType.Alertmanager,
uid: 'prometheusAlertManager',
jsonData: {
implementation: AlertManagerImplementation.prometheus,
},
}),
mimir: mockDataSource<AlertManagerDataSourceJsonData>({
name: 'mimir',
type: DataSourceType.Alertmanager,
uid: 'mimir',
jsonData: {
implementation: AlertManagerImplementation.mimir,
},
}),
};
const ui = {
rootReceiver: byTestId('am-routes-root-receiver'),
rootGroupBy: byTestId('am-routes-root-group-by'),
rootTimings: byTestId('am-routes-root-timings'),
row: byTestId('am-routes-row'),
/** Row of policy tree containing default policy */
rootRouteContainer: byTestId('am-root-route-container'),
/** (deeply) Nested rows of policies under the default/root policy */
row: byTestId('am-route-container'),
editButton: byRole('button', { name: 'Edit' }),
saveButton: byRole('button', { name: 'Save' }),
newChildPolicyButton: byRole('button', { name: /New child policy/ }),
newSiblingPolicyButton: byRole('button', { name: /Add new policy/ }),
setDefaultReceiverCTA: byRole('button', { name: 'Set a default contact point' }),
moreActionsDefaultPolicy: byLabelText(/more actions for default policy/i),
moreActions: byLabelText(/more actions for policy/i),
editButton: byRole('menuitem', { name: 'Edit' }),
editRouteButton: byLabelText('Edit route'),
deleteRouteButton: byLabelText('Delete route'),
newPolicyButton: byRole('button', { name: /Add policy/ }),
newPolicyCTAButton: byRole('button', { name: /Add specific policy/ }),
savePolicyButton: byRole('button', { name: /save policy/i }),
saveButton: byRole('button', { name: /update (default )?policy/i }),
deleteRouteButton: byRole('menuitem', { name: 'Delete' }),
receiverSelect: byTestId('am-receiver-select'),
groupSelect: byTestId('am-group-select'),
@ -101,451 +128,217 @@ const ui = {
groupRepeatContainer: byTestId('am-repeat-interval'),
confirmDeleteModal: byRole('dialog'),
confirmDeleteButton: byLabelText('Confirm Modal Danger Button'),
confirmDeleteButton: byRole('button', { name: /yes, delete policy/i }),
};
const getRootRoute = async () => {
return ui.rootRouteContainer.find();
};
describe('NotificationPolicies', () => {
const subroutes: Route[] = [
{
match: {
sub1matcher1: 'sub1value1',
sub1matcher2: 'sub1value2',
},
match_re: {
sub1matcher3: 'sub1value3',
sub1matcher4: 'sub1value4',
},
group_by: ['sub1group1', 'sub1group2'],
receiver: 'a-receiver',
continue: true,
group_wait: '3s',
group_interval: '2m',
repeat_interval: '1s',
routes: [
{
match: {
sub1sub1matcher1: 'sub1sub1value1',
sub1sub1matcher2: 'sub1sub1value2',
},
match_re: {
sub1sub1matcher3: 'sub1sub1value3',
sub1sub1matcher4: 'sub1sub1value4',
},
group_by: ['sub1sub1group1', 'sub1sub1group2'],
receiver: 'another-receiver',
},
{
match: {
sub1sub2matcher1: 'sub1sub2value1',
sub1sub2matcher2: 'sub1sub2value2',
},
match_re: {
sub1sub2matcher3: 'sub1sub2value3',
sub1sub2matcher4: 'sub1sub2value4',
},
group_by: ['sub1sub2group1', 'sub1sub2group2'],
receiver: 'another-receiver',
},
],
},
{
match: {
sub2matcher1: 'sub2value1',
sub2matcher2: 'sub2value2',
},
match_re: {
sub2matcher3: 'sub2value3',
sub2matcher4: 'sub2value4',
},
receiver: 'another-receiver',
},
];
const emptyRoute: Route = {};
const simpleRoute: Route = {
receiver: 'simple-receiver',
matchers: ['hello=world', 'foo!=bar'],
};
const rootRoute: Route = {
receiver: 'default-receiver',
group_by: ['a-group', 'another-group'],
group_wait: '1s',
group_interval: '2m',
repeat_interval: '3d',
routes: subroutes,
};
const muteInterval: MuteTimeInterval = {
name: 'default-mute',
time_intervals: [
{
times: [{ start_time: '12:00', end_time: '24:00' }],
weekdays: ['monday:friday'],
days_of_month: ['1:7', '-1:-7'],
months: ['january:june'],
years: ['2020:2022'],
},
],
};
beforeEach(() => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
mocks.contextSrv.hasPermission.mockImplementation(() => true);
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: false });
setDataSourceSrv(new MockDataSourceSrv(dataSources));
setupDataSources(...Object.values(dataSources));
grantUserPermissions([
AccessControlAction.AlertingInstanceRead,
AccessControlAction.AlertingInstanceCreate,
AccessControlAction.AlertingInstanceUpdate,
AccessControlAction.AlertingInstancesExternalRead,
AccessControlAction.AlertingInstancesExternalWrite,
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsWrite,
AccessControlAction.AlertingNotificationsExternalRead,
AccessControlAction.AlertingNotificationsExternalWrite,
]);
});
afterEach(() => {
jest.resetAllMocks();
it('loads and shows routes', async () => {
const { alertmanager_config: testConfig } = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
setDataSourceSrv(undefined as unknown as DataSourceSrv);
});
it.skip('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: {},
});
const { route: defaultRoute } = testConfig;
renderNotificationPolicies();
const rootRouteEl = await getRootRoute();
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
expect(ui.rootReceiver.get()).toHaveTextContent(rootRoute.receiver!);
expect(ui.rootGroupBy.get()).toHaveTextContent(rootRoute.group_by!.join(', '));
const rootTimings = ui.rootTimings.get();
expect(rootTimings).toHaveTextContent(rootRoute.group_wait!);
expect(rootTimings).toHaveTextContent(rootRoute.group_interval!);
expect(rootTimings).toHaveTextContent(rootRoute.repeat_interval!);
expect(rootRouteEl).toHaveTextContent(new RegExp(`delivered to ${defaultRoute?.receiver}`, 'i'));
expect(rootRouteEl).toHaveTextContent(new RegExp(`grouped by ${defaultRoute?.group_by?.join(', ')}`, 'i'));
expect(rootRouteEl).toHaveTextContent(/wait 30s to group/i);
expect(rootRouteEl).toHaveTextContent(/wait 5m before sending/i);
expect(rootRouteEl).toHaveTextContent(/repeated every 4h/i);
const rows = await ui.row.findAll();
expect(rows).toHaveLength(2);
expect(rows).toHaveLength(5);
subroutes.forEach((route, index) => {
defaultRoute?.routes?.forEach((route) => {
Object.entries(route.match ?? {}).forEach(([label, value]) => {
expect(rows[index]).toHaveTextContent(`${label}=${value}`);
expect(screen.getByText(`${label} = ${value}`)).toBeInTheDocument();
});
Object.entries(route.match_re ?? {}).forEach(([label, value]) => {
expect(rows[index]).toHaveTextContent(`${label}=~${value}`);
expect(screen.getByText(`${label} =~ ${value}`)).toBeInTheDocument();
});
if (route.group_by) {
expect(rows[index]).toHaveTextContent(route.group_by.join(', '));
expect(rows.some((row) => row?.textContent?.includes(`Grouped by ${route.group_by?.join(', ')}`))).toBe(true);
}
if (route.receiver) {
expect(rows[index]).toHaveTextContent(route.receiver);
expect(rows.some((row) => row?.textContent?.includes(`Delivered to ${route.receiver}`))).toBe(true);
}
});
});
it.skip('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);
});
it('can edit root route if one is already defined', async () => {
const { user } = renderNotificationPolicies();
expect(await ui.rootReceiver.find()).toHaveTextContent('default');
expect(ui.rootGroupBy.get()).toHaveTextContent('alertname');
let rootRoute = await getRootRoute();
// open root route for editing
const rootRouteContainer = await ui.rootRouteContainer.find();
await user.click(ui.editButton.get(rootRouteContainer));
expect(rootRoute).toHaveTextContent('default policy');
expect(rootRoute).toHaveTextContent(/delivered to grafana-default-email/i);
expect(rootRoute).toHaveTextContent(/grouped by alertname/i);
await openDefaultPolicyEditModal();
// configure receiver & group by
const receiverSelect = await ui.receiverSelect.find();
await clickSelectOption(receiverSelect, 'critical');
// The contact points are fetched from the k8s API, which we aren't overriding here
// when we use a different
await clickSelectOption(receiverSelect, 'lotsa-emails');
const groupSelect = ui.groupSelect.get();
await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}');
// configure timing intervals
await user.click(byText('Timing options').get(rootRouteContainer));
await user.click(screen.getByText(/timing options/i));
await updateTiming(ui.groupWaitContainer.get(), '1', 'Minutes');
await updateTiming(ui.groupIntervalContainer.get(), '4', 'Minutes');
await updateTiming(ui.groupRepeatContainer.get(), '5', 'Hours');
await updateTiming(ui.groupWaitContainer.get(), '1m');
await updateTiming(ui.groupIntervalContainer.get(), '4m');
await updateTiming(ui.groupRepeatContainer.get(), '5h');
//save
await user.click(ui.saveButton.get(rootRouteContainer));
await user.click(await screen.findByRole('button', { name: /update default policy/i }));
// 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',
mute_time_intervals: [],
},
templates: [],
},
template_files: {},
});
expect(await screen.findByText(/updated notification policies/i)).toBeInTheDocument();
// check that new config values are rendered
await waitFor(() => expect(ui.rootReceiver.query()).toHaveTextContent('critical'));
expect(ui.rootGroupBy.get()).toHaveTextContent('alertname, namespace');
rootRoute = await getRootRoute();
expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i);
expect(rootRoute).toHaveTextContent(/grouped by alertname, namespace/i);
});
it.skip('can edit root route if one is not defined yet', async () => {
mocks.api.fetchAlertManagerConfig.mockResolvedValue({
it('can edit root route if one is not defined yet', async () => {
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, {
alertmanager_config: {
receivers: [{ name: 'default' }],
route: {},
receivers: [{ name: 'grafana-default-email' }],
},
template_files: {},
});
const { user } = renderNotificationPolicies();
// open root route for editing
const rootRouteContainer = await ui.rootRouteContainer.find();
await user.click(ui.editButton.get(rootRouteContainer));
await openDefaultPolicyEditModal();
// configure receiver & group by
const receiverSelect = await ui.receiverSelect.find();
await clickSelectOption(receiverSelect, 'default');
await clickSelectOption(receiverSelect, 'lotsa-emails');
const groupSelect = ui.groupSelect.get();
await user.type(byRole('combobox').get(groupSelect), 'severity{enter}');
await user.type(byRole('combobox').get(groupSelect), 'namespace{enter}');
//save
await user.click(ui.saveButton.get(rootRouteContainer));
await user.click(await screen.findByRole('button', { name: /update default policy/i }));
// wait for it to go out of edit mode
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
expect(await screen.findByText(/updated notification policies/i)).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: defaultGroupBy.concat(['severity', 'namespace']),
receiver: 'default',
routes: [],
mute_time_intervals: [],
},
},
template_files: {},
});
const rootRoute = await getRootRoute();
expect(rootRoute).toHaveTextContent(/delivered to lotsa-emails/i);
expect(rootRoute).toHaveTextContent(/grouped by severity, namespace/i);
});
it('hides create and edit button if user does not have permission', async () => {
mocks.contextSrv.hasPermission.mockImplementation((action) =>
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsRead].includes(
action as AccessControlAction
)
);
grantUserPermissions([
AccessControlAction.AlertingInstanceRead,
AccessControlAction.AlertingInstancesExternalRead,
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
]);
renderNotificationPolicies();
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
const { user } = renderNotificationPolicies();
expect(ui.newPolicyButton.query()).not.toBeInTheDocument();
expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument();
expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument();
await user.click(await ui.moreActionsDefaultPolicy.find());
expect(ui.editButton.query()).not.toBeInTheDocument();
});
it('Show error message if loading Alertmanager config fails', async () => {
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
status: 500,
data: {
message: "Alertmanager has exploded. it's gone. Forget about it.",
},
});
jest.spyOn(alertmanagerApi, 'useGetAlertmanagerAlertGroupsQuery').mockImplementation(() => ({
currentData: [],
refetch: jest.fn(),
}));
makeAllAlertmanagerConfigFetchFail(getErrorResponse("Alertmanager has exploded. it's gone. Forget about it."));
renderNotificationPolicies();
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
await screen.findByText(/error loading alertmanager config/i);
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();
expect(ui.rootRouteContainer.query()).not.toBeInTheDocument();
});
it.skip('Converts matchers to object_matchers for grafana alertmanager', async () => {
const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
continue: false,
receiver: 'default',
group_by: ['alertname'],
routes: [simpleRoute],
group_interval: '4m',
group_wait: '1m',
repeat_interval: '5h',
},
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);
});
it('Converts matchers to object_matchers for grafana alertmanager', async () => {
const { user } = renderNotificationPolicies(GRAFANA_RULES_SOURCE_NAME);
expect(await ui.rootReceiver.find()).toHaveTextContent('default');
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled();
// Toggle a save to test new object_matchers
const rootRouteContainer = await ui.rootRouteContainer.find();
await user.click(ui.editButton.get(rootRouteContainer));
await user.click(ui.saveButton.get(rootRouteContainer));
const policyIndex = 0;
await openEditModal(policyIndex);
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
// Save policy to test that format is converted to object_matchers
await user.click(await ui.saveButton.find());
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled();
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(GRAFANA_RULES_SOURCE_NAME, {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
continue: false,
group_by: ['alertname'],
group_interval: '4m',
group_wait: '1m',
receiver: 'default',
repeat_interval: '5h',
mute_time_intervals: [],
routes: [
{
continue: false,
group_by: [],
object_matchers: [
['hello', '=', 'world'],
['foo', '!=', 'bar'],
],
receiver: 'simple-receiver',
mute_time_intervals: [],
routes: [],
},
],
},
templates: [],
},
template_files: {},
});
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
const updatedConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].object_matchers).toMatchSnapshot();
});
it.skip('Should be able to delete an empty route', async () => {
const routeConfig = {
continue: false,
receiver: 'default',
group_by: ['alertname'],
routes: [emptyRoute],
group_interval: '4m',
group_wait: '1m',
repeat_interval: '5h',
mute_time_intervals: [],
};
it('Should be able to delete an empty route', async () => {
const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: routeConfig,
templates: [],
route: {
routes: [{}],
},
},
template_files: {},
};
mocks.api.fetchAlertManagerConfig.mockImplementation(() => {
return Promise.resolve(defaultConfig);
});
mocks.api.updateAlertManagerConfig.mockResolvedValue(Promise.resolve());
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
const { user } = renderNotificationPolicies(GRAFANA_RULES_SOURCE_NAME);
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled());
const deleteButtons = await ui.deleteRouteButton.findAll();
expect(deleteButtons).toHaveLength(1);
await user.click(await ui.moreActions.find());
const deleteButtons = await ui.deleteRouteButton.find();
await user.click(deleteButtons[0]);
await user.click(deleteButtons);
const confirmDeleteButton = ui.confirmDeleteButton.get(ui.confirmDeleteModal.get());
expect(confirmDeleteButton).toBeInTheDocument();
await user.click(confirmDeleteButton);
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith<[string, AlertManagerCortexConfig]>(
GRAFANA_RULES_SOURCE_NAME,
{
...defaultConfig,
alertmanager_config: {
...defaultConfig.alertmanager_config,
route: {
...routeConfig,
routes: [],
},
},
}
);
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
expect(ui.row.query()).not.toBeInTheDocument();
});
it.skip('Keeps matchers for non-grafana alertmanager sources', async () => {
const defaultConfig: AlertManagerCortexConfig = {
it('Keeps matchers for non-grafana alertmanager sources', async () => {
setAlertmanagerConfig(dataSources.am.uid, {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
continue: false,
receiver: 'default',
group_by: ['alertname'],
routes: [simpleRoute],
routes: [
{
receiver: 'simple-receiver',
matchers: ['hello=world', 'foo!=bar'],
},
],
group_interval: '4m',
group_wait: '1m',
repeat_interval: '5h',
@ -553,79 +346,38 @@ describe('NotificationPolicies', () => {
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);
});
const { user } = renderNotificationPolicies(dataSources.am.name);
expect(await ui.rootReceiver.find()).toHaveTextContent('default');
expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalled();
// Toggle a save to test new object_matchers
const rootRouteContainer = await ui.rootRouteContainer.find();
await user.click(ui.editButton.get(rootRouteContainer));
await user.click(ui.saveButton.get(rootRouteContainer));
const policyIndex = 0;
await openEditModal(policyIndex);
await waitFor(() => expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument());
// Save policy to test that format is NOT converted
await user.click(await ui.saveButton.find());
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled();
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(dataSources.am.name, {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
continue: false,
group_by: ['alertname'],
group_interval: '4m',
group_wait: '1m',
matchers: [],
receiver: 'default',
repeat_interval: '5h',
mute_time_intervals: [],
routes: [
{
continue: false,
group_by: [],
matchers: ['hello=world', 'foo!=bar'],
receiver: 'simple-receiver',
routes: [],
mute_time_intervals: [],
},
],
},
templates: [],
},
template_files: {},
});
const updatedConfig = getAlertmanagerConfig(dataSources.am.uid);
expect(updatedConfig.alertmanager_config.route?.routes?.[policyIndex].matchers).toMatchSnapshot();
});
it.skip('Prometheus Alertmanager routes cannot be edited', async () => {
mocks.api.fetchStatus.mockResolvedValue({
it('Prometheus Alertmanager routes cannot be edited', async () => {
setAlertmanagerStatus(dataSources.promAlertManager.uid, {
...someCloudAlertManagerStatus,
config: someCloudAlertManagerConfig.alertmanager_config,
});
renderNotificationPolicies(dataSources.promAlertManager.name);
const rootRouteContainer = await ui.rootRouteContainer.find();
expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument();
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
const rows = await ui.row.findAll();
expect(rows).toHaveLength(2);
expect(ui.editRouteButton.query()).not.toBeInTheDocument();
expect(ui.deleteRouteButton.query()).not.toBeInTheDocument();
expect(ui.saveButton.query()).not.toBeInTheDocument();
expect(mocks.api.fetchAlertManagerConfig).not.toHaveBeenCalled();
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
expect(ui.moreActions.query()).not.toBeInTheDocument();
expect(ui.moreActionsDefaultPolicy.query()).not.toBeInTheDocument();
});
it('Prometheus Alertmanager has no CTA button if there are no specific policies', async () => {
mocks.api.fetchStatus.mockResolvedValue({
setAlertmanagerStatus(dataSources.promAlertManager.uid, {
...someCloudAlertManagerStatus,
config: {
...someCloudAlertManagerConfig.alertmanager_config,
@ -636,102 +388,39 @@ describe('NotificationPolicies', () => {
},
});
jest.spyOn(alertmanagerApi, 'useGetAlertmanagerAlertGroupsQuery').mockImplementation(() => ({
currentData: [],
refetch: jest.fn(),
}));
renderNotificationPolicies(dataSources.promAlertManager.name);
const rootRouteContainer = await ui.rootRouteContainer.find();
await waitFor(() =>
expect(within(rootRouteContainer).getByTestId('matching-instances')).toHaveTextContent('0instance')
);
expect(ui.editButton.query(rootRouteContainer)).not.toBeInTheDocument();
expect(ui.newPolicyCTAButton.query()).not.toBeInTheDocument();
expect(mocks.api.fetchAlertManagerConfig).not.toHaveBeenCalled();
expect(mocks.api.fetchStatus).toHaveBeenCalledTimes(1);
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
expect(ui.newChildPolicyButton.query()).not.toBeInTheDocument();
expect(ui.newSiblingPolicyButton.query()).not.toBeInTheDocument();
});
it.skip('Can add a mute timing to a route', async () => {
const defaultConfig: AlertManagerCortexConfig = {
alertmanager_config: {
receivers: [{ name: 'default' }, { name: 'critical' }],
route: {
continue: false,
receiver: 'default',
group_by: ['alertname'],
routes: [simpleRoute],
group_interval: '4m',
group_wait: '1m',
repeat_interval: '5h',
},
templates: [],
mute_time_intervals: [muteInterval],
},
template_files: {},
};
it('Can add a mute timing to a route', async () => {
const { user } = renderNotificationPolicies();
const currentConfig = { current: defaultConfig };
mocks.api.updateAlertManagerConfig.mockImplementation((amSourceName, newConfig) => {
currentConfig.current = newConfig;
return Promise.resolve();
});
mocks.api.fetchAlertManagerConfig.mockResolvedValue(defaultConfig);
const { user } = renderNotificationPolicies(dataSources.am.name);
const rows = await ui.row.findAll();
expect(rows).toHaveLength(1);
await user.click(ui.editRouteButton.get(rows[0]));
await openEditModal(0);
const muteTimingSelect = ui.muteTimingSelect.get();
await clickSelectOption(muteTimingSelect, 'default-mute');
expect(muteTimingSelect).toHaveTextContent('default-mute');
await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_HAPPY_PATH);
await clickSelectOption(muteTimingSelect, TIME_INTERVAL_NAME_FILE_PROVISIONED);
const savePolicyButton = ui.savePolicyButton.get();
expect(savePolicyButton).toBeInTheDocument();
await user.click(ui.saveButton.get());
await user.click(savePolicyButton);
expect(await screen.findByRole('status')).toHaveTextContent(/updated notification policies/i);
await waitFor(() => expect(savePolicyButton).not.toBeInTheDocument());
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalled();
expect(mocks.api.updateAlertManagerConfig).toHaveBeenCalledWith(dataSources.am.name, {
...defaultConfig,
alertmanager_config: {
...defaultConfig.alertmanager_config,
route: {
...defaultConfig.alertmanager_config.route,
mute_time_intervals: [],
matchers: [],
routes: [
{
...simpleRoute,
mute_time_intervals: [muteInterval.name],
routes: [],
continue: false,
group_by: [],
},
],
},
},
});
const policy = (await ui.row.findAll())[0];
expect(policy).toHaveTextContent(
`Muted when ${TIME_INTERVAL_NAME_HAPPY_PATH}, ${TIME_INTERVAL_NAME_FILE_PROVISIONED}`
);
});
it.skip('Shows an empty config when config returns an error and the AM supports lazy config initialization', async () => {
mocks.api.discoverAlertmanagerFeatures.mockResolvedValue({ lazyConfigInit: true });
makeAllAlertmanagerConfigFetchFail(getErrorResponse('alertmanager storage object not found'));
setAlertmanagerStatus(dataSources.mimir.uid, someCloudAlertManagerStatus);
renderNotificationPolicies(dataSources.mimir.name);
mocks.api.fetchAlertManagerConfig.mockRejectedValue({
message: 'alertmanager storage object not found',
});
renderNotificationPolicies();
await waitFor(() => expect(mocks.api.fetchAlertManagerConfig).toHaveBeenCalledTimes(1));
expect(ui.rootReceiver.query()).toBeInTheDocument();
expect(ui.setDefaultReceiverCTA.query()).toBeInTheDocument();
expect(await ui.rootRouteContainer.find()).toBeInTheDocument();
});
});
@ -806,19 +495,3 @@ describe('findRoutesMatchingFilters', () => {
expect(matchingRoutes).toMatchSnapshot();
});
});
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
const user = userEvent.setup();
await user.click(byRole('combobox').get(selectElement));
await selectOptionInTest(selectElement, optionText);
};
const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit: string): Promise<void> => {
const user = userEvent.setup();
const input = byRole('textbox').get(selectElement);
const select = byRole('combobox').get(selectElement);
await user.clear(input);
await user.type(input, value);
await user.click(select);
await selectOptionInTest(selectElement, timeUnit);
};

View File

@ -4,6 +4,7 @@ import { useAsyncFn } from 'react-use';
import { GrafanaTheme2, UrlQueryMap } from '@grafana/data';
import { Alert, LoadingPlaceholder, Stack, Tab, TabContent, TabsBar, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useMuteTimings } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { ObjectMatcher, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
@ -52,6 +53,7 @@ enum ActiveTab {
const AmRoutes = () => {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const appNotification = useAppNotification();
const { useGetAlertmanagerAlertGroupsQuery } = alertmanagerApi;
@ -175,11 +177,11 @@ const AmRoutes = () => {
},
oldConfig: result,
alertManagerSourceName: selectedAlertmanager!,
successMessage: 'Updated notification policies',
})
)
.unwrap()
.then(() => {
appNotification.success('Updated notification policies');
if (selectedAlertmanager) {
refetchAlertGroups();
}

View File

@ -1,5 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotificationPolicies Converts matchers to object_matchers for grafana alertmanager 1`] = `
[
[
"sub1matcher1",
"=",
"sub1value1",
],
[
"sub1matcher2",
"=",
"sub1value2",
],
[
"sub1matcher3",
"=~",
"sub1value3",
],
[
"sub1matcher4",
"=~",
"sub1value4",
],
]
`;
exports[`NotificationPolicies Keeps matchers for non-grafana alertmanager sources 1`] = `
[
"hello="world"",
"foo!="bar"",
]
`;
exports[`findRoutesMatchingFilters should not match non-existing 1`] = `
{
"filtersApplied": true,

View File

@ -1,106 +0,0 @@
{
"template_files": {
"slack-template": "{{ define \"slack-template\" }} Custom slack template {{ end }}",
"custom-email": "{{ define \"custom-email\" }} Custom email template {{ end }}",
"provisioned-template": "{{ define \"provisioned-template\" }} Custom provisioned template {{ end }}",
"template with spaces": "{{ define \"template with spaces\" }} Custom template with spaces in the name {{ end }}",
"misconfigured-template": "{{ define \"misconfigured template\" }} Template that is defined in template_files but not templates {{ end }}",
"misconfigured and provisioned": "{{ define \"misconfigured and provisioned template\" }} Provisioned template that is defined in template_files but not templates {{ end }}"
},
"template_file_provenances": {
"provisioned-template": "api",
"misconfigured and provisioned": "api"
},
"alertmanager_config": {
"route": {
"receiver": "grafana-default-email",
"routes": [
{
"receiver": "provisioned-contact-point"
}
]
},
"receivers": [
{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [
{
"uid": "xeKQrBrnk",
"name": "grafana-default-email",
"type": "email",
"disableResolveMessage": false,
"settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false },
"secureFields": {}
}
]
},
{
"name": "provisioned-contact-point",
"grafana_managed_receiver_configs": [
{
"uid": "s8SdCVjnk",
"name": "provisioned-contact-point",
"type": "email",
"disableResolveMessage": false,
"settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false },
"secureFields": {},
"provenance": "api"
}
]
},
{
"name": "lotsa-emails",
"grafana_managed_receiver_configs": [
{
"uid": "af306c96-35a2-4d6e-908a-4993e245dbb2",
"name": "lotsa-emails",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com",
"singleEmail": false
},
"secureFields": {}
}
]
},
{
"name": "Slack with multiple channels",
"grafana_managed_receiver_configs": [
{
"uid": "c02ad56a-31da-46b9-becb-4348ec0890fd",
"name": "Slack with multiple channels",
"type": "slack",
"disableResolveMessage": false,
"settings": { "recipient": "test-alerts" },
"secureFields": { "token": true }
},
{
"uid": "b286a3be-f690-49e2-8605-b075cbace2df",
"name": "Slack with multiple channels",
"type": "slack",
"disableResolveMessage": false,
"settings": { "recipient": "test-alerts2" },
"secureFields": { "token": true }
}
]
},
{
"name": "OnCall Conctact point",
"grafana_managed_receiver_configs": [
{
"name": "Oncall-integration",
"type": "oncall",
"settings": {
"url": "https://oncall-endpoint.example.com"
},
"disableResolveMessage": false
}
]
}
],
"templates": ["slack-template", "custom-email", "provisioned-template", "template with spaces"],
"time_intervals": [],
"mute_time_intervals": []
}
}

View File

@ -3,10 +3,8 @@ import { render, screen, userEvent, within } from 'test/test-utils';
import { config } from '@grafana/runtime';
import { defaultConfig } from 'app/features/alerting/unified/MuteTimings.test';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import {
setGrafanaAlertmanagerConfig,
setMuteTimingsListError,
} from 'app/features/alerting/unified/mocks/server/configure';
import { setMuteTimingsListError } from 'app/features/alerting/unified/mocks/server/configure';
import { setAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
@ -31,7 +29,7 @@ setupMswServer();
describe('MuteTimingsTable', () => {
describe('with necessary permissions', () => {
beforeEach(() => {
setGrafanaAlertmanagerConfig(defaultConfig);
setAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME, defaultConfig);
config.featureToggles.alertingApiServer = false;
grantUserPermissions([
AccessControlAction.AlertingNotificationsRead,

View File

@ -133,8 +133,8 @@ describe('Policy', () => {
expect(within(firstPolicy).getByTestId('continue-matching')).toBeInTheDocument();
// expect(within(firstPolicy).getByTestId('matching-instances')).toHaveTextContent('0instances');
expect(within(firstPolicy).getByTestId('contact-point')).toHaveTextContent('provisioned-contact-point');
expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted whenmt-1');
expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active whenmt-2');
expect(within(firstPolicy).getByTestId('mute-timings')).toHaveTextContent('Muted when mt-1');
expect(within(firstPolicy).getByTestId('active-timings')).toHaveTextContent('Active when mt-2');
expect(within(firstPolicy).getByTestId('inherited-properties')).toHaveTextContent('Inherited2 properties');
// second custom policy should be correct

View File

@ -23,6 +23,7 @@ import {
} from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import ConditionalWrap from 'app/features/alerting/unified/components/ConditionalWrap';
import MoreButton from 'app/features/alerting/unified/components/MoreButton';
import { PrimaryText } from 'app/features/alerting/unified/components/common/TextVariants';
import {
AlertmanagerGroup,
@ -308,12 +309,8 @@ const Policy = (props: PolicyComponentProps) => {
)}
{dropdownMenuActions.length > 0 && (
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
<Button
icon="ellipsis-h"
variant="secondary"
size="sm"
type="button"
aria-label="more-actions"
<MoreButton
aria-label={isDefaultPolicy ? 'more actions for default policy' : 'more actions for policy'}
data-testid="more-actions"
/>
</Dropdown>
@ -469,7 +466,7 @@ function MetadataRow({
{contactPoint && (
<MetaText icon="at" data-testid="contact-point">
<span>
<Trans i18nKey="alerting.policies.metadata.delivered-to">Delivered to</Trans>
<Trans i18nKey="alerting.policies.metadata.delivered-to">Delivered to</Trans>{' '}
</span>
<ContactPointsHoverDetails
alertManagerSourceName={alertManagerSourceName}
@ -483,7 +480,7 @@ function MetadataRow({
{customGrouping && (
<MetaText icon="layer-group" data-testid="grouping">
<span>
<Trans i18nKey="alerting.policies.metadata.grouped-by">Grouped by</Trans>
<Trans i18nKey="alerting.policies.metadata.grouped-by">Grouped by</Trans>{' '}
</span>
<Text color="primary">{groupBy.join(', ')}</Text>
</MetaText>
@ -507,7 +504,7 @@ function MetadataRow({
{hasMuteTimings && (
<MetaText icon="calendar-slash" data-testid="mute-timings">
<span>
<Trans i18nKey="alerting.policies.metadata.mute-time">Muted when</Trans>
<Trans i18nKey="alerting.policies.metadata.mute-time">Muted when</Trans>{' '}
</span>
<TimeIntervals timings={muteTimings} alertManagerSourceName={alertManagerSourceName} />
</MetaText>
@ -515,7 +512,7 @@ function MetadataRow({
{hasActiveTimings && (
<MetaText icon="calendar-alt" data-testid="active-timings">
<span>
<Trans i18nKey="alerting.policies.metadata.active-time">Active when</Trans>
<Trans i18nKey="alerting.policies.metadata.active-time">Active when</Trans>{' '}
</span>
<TimeIntervals timings={activeTimings} alertManagerSourceName={alertManagerSourceName} />
</MetaText>

View File

@ -3,7 +3,7 @@ import { Route } from 'react-router';
import { render, screen } from 'test/test-utils';
import { byLabelText, byPlaceholderText, byRole, byTestId } from 'testing-library-selector';
import { makeGrafanaAlertmanagerConfigUpdateFail } from 'app/features/alerting/unified/mocks/server/configure';
import { makeAlertmanagerConfigUpdateFail } from 'app/features/alerting/unified/mocks/server/configure';
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { AccessControlAction } from 'app/types';
@ -108,7 +108,7 @@ describe('alerting API server disabled', () => {
});
it('does not redirect when creating contact point and API errors', async () => {
makeGrafanaAlertmanagerConfigUpdateFail();
makeAlertmanagerConfigUpdateFail();
const { user } = renderForm();
await user.type(await ui.inputs.name.find(), 'receiver that should fail');

View File

@ -2,9 +2,9 @@ import { ReactNode } from 'react';
import { render, screen, userEvent } from 'test/test-utils';
import { CodeEditorProps } from '@grafana/ui/src/components/Monaco/types';
import alertmanagerConfigMock from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions } from 'app/features/alerting/unified/mocks';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
@ -15,6 +15,8 @@ import { AccessControlAction, NotificationChannelOption } from 'app/types';
import { getTemplateOptions, TemplatesPicker } from './TemplateSelector';
import { parseTemplates } from './utils';
const alertmanagerConfigMock = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
jest.mock('@grafana/ui', () => ({
...jest.requireActual('@grafana/ui'),
CodeEditor: ({ value, onChange }: CodeEditorProps) => (

View File

@ -5,6 +5,10 @@ import { DataSourceInstanceSettings } from '@grafana/data';
import { setBackendSrv } from '@grafana/runtime';
import { AlertGroupUpdated } from 'app/features/alerting/unified/api/alertRuleApi';
import allHandlers from 'app/features/alerting/unified/mocks/server/all-handlers';
import {
setupAlertmanagerConfigMapDefaultState,
setupAlertmanagerStatusMapDefaultState,
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { DashboardDTO, FolderDTO, OrgUser } from 'app/types';
import {
PromBuildInfoResponse,
@ -289,6 +293,10 @@ export function setupMswServer() {
afterEach(() => {
server.resetHandlers();
// Reset any other necessary mock entities/state
setupAlertmanagerConfigMapDefaultState();
setupAlertmanagerStatusMapDefaultState();
});
afterAll(() => {

View File

@ -512,6 +512,7 @@ export const someGrafanaAlertManagerConfig: AlertManagerCortexConfig = {
},
};
/** @deprecated Move into alertmanager status entities */
export const someCloudAlertManagerStatus: AlertmanagerStatus = {
cluster: {
peers: [],
@ -543,6 +544,7 @@ export const someCloudAlertManagerStatus: AlertmanagerStatus = {
},
};
/** @deprecated Move into alertmanager config entities */
export const someCloudAlertManagerConfig: AlertManagerCortexConfig = {
template_files: {
'foo template': 'foo content',

View File

@ -4,11 +4,9 @@ import { config } from '@grafana/runtime';
import server, { mockFeatureDiscoveryApi } from 'app/features/alerting/unified/mockApi';
import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks';
import {
ALERTMANAGER_UPDATE_ERROR_RESPONSE,
getAlertmanagerConfigHandler,
getGrafanaAlertmanagerConfigHandler,
grafanaAlertingConfigurationStatusHandler,
updateGrafanaAlertmanagerConfigHandler,
updateAlertmanagerConfigHandler,
} from 'app/features/alerting/unified/mocks/server/handlers/alertmanagers';
import { getFolderHandler } from 'app/features/alerting/unified/mocks/server/handlers/folders';
import { listNamespacedTimeIntervalHandler } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
@ -18,7 +16,7 @@ import {
} from 'app/features/alerting/unified/mocks/server/handlers/plugins';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings';
import { AlertManagerCortexConfig, AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { FolderDTO } from 'app/types';
import { setupDataSources } from '../../testSetup/datasources';
@ -60,20 +58,6 @@ export const setFolderResponse = (response: Partial<FolderDTO>) => {
server.use(handler);
};
/**
* Makes the mock server respond with different Grafana Alertmanager config
*/
export const setGrafanaAlertmanagerConfig = (config: AlertManagerCortexConfig) => {
server.use(getGrafanaAlertmanagerConfigHandler(config));
};
/**
* Makes the mock server respond with different (other) Alertmanager config
*/
export const setAlertmanagerConfig = (config: AlertManagerCortexConfig) => {
server.use(getAlertmanagerConfigHandler(config));
};
/**
* Makes the mock server respond with different responses for updating a ruler namespace
*/
@ -139,7 +123,26 @@ export const disablePlugin = (pluginId: SupportedPlugin) => {
server.use(getDisabledPluginHandler(pluginId));
};
/** Get an error response for use in a API response, in the format:
* ```
* {
* message: string,
* }
* ```
*/
export const getErrorResponse = (message: string, status = 500) => HttpResponse.json({ message }, { status });
const defaultError = getErrorResponse('Unknown error');
/** Make alertmanager config update fail */
export const makeGrafanaAlertmanagerConfigUpdateFail = () => {
server.use(updateGrafanaAlertmanagerConfigHandler(ALERTMANAGER_UPDATE_ERROR_RESPONSE));
export const makeAlertmanagerConfigUpdateFail = (
responseOverride: ReturnType<typeof getErrorResponse> = defaultError
) => {
server.use(updateAlertmanagerConfigHandler(responseOverride));
};
/** Make fetching alertmanager config fail */
export const makeAllAlertmanagerConfigFetchFail = (
responseOverride: ReturnType<typeof getErrorResponse> = defaultError
) => {
server.use(getAlertmanagerConfigHandler(responseOverride));
};

View File

@ -0,0 +1,190 @@
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
const grafanaAlertmanagerConfig: AlertManagerCortexConfig = {
template_files: {
'slack-template': '{{ define "slack-template" }} Custom slack template {{ end }}',
'custom-email': '{{ define "custom-email" }} Custom email template {{ end }}',
'provisioned-template': '{{ define "provisioned-template" }} Custom provisioned template {{ end }}',
'template with spaces': '{{ define "template with spaces" }} Custom template with spaces in the name {{ end }}',
'misconfigured-template':
'{{ define "misconfigured template" }} Template that is defined in template_files but not templates {{ end }}',
'misconfigured and provisioned':
'{{ define "misconfigured and provisioned template" }} Provisioned template that is defined in template_files but not templates {{ end }}',
},
template_file_provenances: {
'provisioned-template': 'api',
'misconfigured and provisioned': 'api',
},
alertmanager_config: {
route: {
group_by: ['alertname'],
receiver: 'grafana-default-email',
routes: [
{
match: {
sub1matcher1: 'sub1value1',
sub1matcher2: 'sub1value2',
},
match_re: {
sub1matcher3: 'sub1value3',
sub1matcher4: 'sub1value4',
},
group_by: ['sub1group1', 'sub1group2'],
receiver: 'a-receiver',
continue: true,
group_wait: '3s',
group_interval: '2m',
repeat_interval: '3m',
routes: [
{
match: {
sub1sub1matcher1: 'sub1sub1value1',
sub1sub1matcher2: 'sub1sub1value2',
},
match_re: {
sub1sub1matcher3: 'sub1sub1value3',
sub1sub1matcher4: 'sub1sub1value4',
},
group_by: ['sub1sub1group1', 'sub1sub1group2'],
receiver: 'another-receiver',
},
{
match: {
sub1sub2matcher1: 'sub1sub2value1',
sub1sub2matcher2: 'sub1sub2value2',
},
match_re: {
sub1sub2matcher3: 'sub1sub2value3',
sub1sub2matcher4: 'sub1sub2value4',
},
group_by: ['sub1sub2group1', 'sub1sub2group2'],
receiver: 'another-receiver',
},
],
},
{
match: {
sub2matcher1: 'sub2value1',
sub2matcher2: 'sub2value2',
},
match_re: {
sub2matcher3: 'sub2value3',
sub2matcher4: 'sub2value4',
},
receiver: 'another-receiver',
},
{
receiver: 'provisioned-contact-point',
},
],
},
receivers: [
{
name: 'grafana-default-email',
grafana_managed_receiver_configs: [
{
uid: 'xeKQrBrnk',
name: 'grafana-default-email',
type: 'email',
disableResolveMessage: false,
settings: {
addresses: 'gilles.demey@grafana.com',
singleEmail: false,
},
secureFields: {},
},
],
},
{
name: 'provisioned-contact-point',
grafana_managed_receiver_configs: [
{
uid: 's8SdCVjnk',
name: 'provisioned-contact-point',
type: 'email',
disableResolveMessage: false,
settings: {
addresses: 'gilles.demey@grafana.com',
singleEmail: false,
},
secureFields: {},
provenance: 'api',
},
],
},
{
name: 'lotsa-emails',
grafana_managed_receiver_configs: [
{
uid: 'af306c96-35a2-4d6e-908a-4993e245dbb2',
name: 'lotsa-emails',
type: 'email',
disableResolveMessage: false,
settings: {
addresses:
'gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com',
singleEmail: false,
},
secureFields: {},
},
],
},
{
name: 'Slack with multiple channels',
grafana_managed_receiver_configs: [
{
uid: 'c02ad56a-31da-46b9-becb-4348ec0890fd',
name: 'Slack with multiple channels',
type: 'slack',
disableResolveMessage: false,
settings: {
recipient: 'test-alerts',
},
secureFields: {
token: true,
},
},
{
uid: 'b286a3be-f690-49e2-8605-b075cbace2df',
name: 'Slack with multiple channels',
type: 'slack',
disableResolveMessage: false,
settings: {
recipient: 'test-alerts2',
},
secureFields: {
token: true,
},
},
],
},
{
name: 'OnCall Conctact point',
grafana_managed_receiver_configs: [
{
name: 'Oncall-integration',
type: 'oncall',
settings: {
url: 'https://oncall-endpoint.example.com',
},
disableResolveMessage: false,
},
],
},
],
templates: ['slack-template', 'custom-email', 'provisioned-template', 'template with spaces'],
time_intervals: [
{
name: 'Some interval',
time_intervals: [],
},
{
name: 'A provisioned interval',
time_intervals: [],
},
],
mute_time_intervals: [],
},
} as const;
export default grafanaAlertmanagerConfig;

View File

@ -0,0 +1,62 @@
import grafanaAlertmanagerConfig from 'app/features/alerting/unified/mocks/server/entities/alertmanager-config/grafana-alertmanager-config';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { AlertManagerCortexConfig, AlertmanagerStatus } from 'app/plugins/datasource/alertmanager/types';
//////////////////////////
// Alertmanager configs //
//////////////////////////
/** **INITIAL** state of alertmanager configs for different scenarios */
const ALERTMANAGER_CONFIGS: Record<string, AlertManagerCortexConfig> = {
// TODO in followup PR: Move mock AM config to TS file rather than JSON
[GRAFANA_RULES_SOURCE_NAME]: grafanaAlertmanagerConfig,
};
let ALERTMANAGER_CONFIG_MAP: Map<string, AlertManagerCortexConfig> = new Map(Object.entries(ALERTMANAGER_CONFIGS));
/** Setup/reset alertmanager configs for our mock server */
export const setupAlertmanagerConfigMapDefaultState = () => {
ALERTMANAGER_CONFIG_MAP = new Map(Object.entries(ALERTMANAGER_CONFIGS));
};
/**
* "Save" a new individual alertmanager config to our internal map
*/
export const setAlertmanagerConfig = (alertmanagerName: string, config: AlertManagerCortexConfig) => {
ALERTMANAGER_CONFIG_MAP.set(alertmanagerName, config);
};
/**
* Get alertmanager config from internal map, for use in assertions
*/
export const getAlertmanagerConfig = (alertmanagerName: string) => {
return ALERTMANAGER_CONFIG_MAP.get(alertmanagerName)!;
};
///////////////////////////
// Alertmanager statuses //
///////////////////////////
/** **INITIAL** state of alertmanager configs for different scenarios */
const ALERTMANAGER_STATUSES: Record<string, AlertmanagerStatus> = {};
let ALERTMANAGER_STATUS_MAP: Map<string, AlertmanagerStatus> = new Map(Object.entries(ALERTMANAGER_STATUSES));
/** Setup/reset alertmanager statuses for our mock server */
export const setupAlertmanagerStatusMapDefaultState = () => {
ALERTMANAGER_STATUS_MAP = new Map(Object.entries(ALERTMANAGER_STATUSES));
};
/**
* "Save" a new individual alertmanager config to our internal map
*/
export const setAlertmanagerStatus = (alertmanagerName: string, config: AlertmanagerStatus) => {
ALERTMANAGER_STATUS_MAP.set(alertmanagerName, config);
};
/**
* Get alertmanager config from internal map, for use in assertions
*/
export const getAlertmanagerStatus = (alertmanagerName: string) => {
return ALERTMANAGER_STATUS_MAP.get(alertmanagerName)!;
};

View File

@ -1,9 +1,13 @@
import { http, HttpResponse } from 'msw';
import { http, HttpResponse, JsonBodyType, StrictResponse } from 'msw';
import alertmanagerConfigMock from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import receiversMock from 'app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json';
import { MOCK_SILENCE_ID_EXISTING, mockAlertmanagerAlert } from 'app/features/alerting/unified/mocks';
import { defaultGrafanaAlertingConfigurationStatusResponse } from 'app/features/alerting/unified/mocks/alertmanagerApi';
import {
getAlertmanagerConfig,
getAlertmanagerStatus,
setAlertmanagerConfig,
} from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER } from 'app/features/alerting/unified/mocks/server/handlers/datasources';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { AlertManagerCortexConfig, AlertState } from 'app/plugins/datasource/alertmanager/types';
@ -58,11 +62,32 @@ export const alertmanagerAlertsListHandler = () =>
]);
});
export const getGrafanaAlertmanagerConfigHandler = (config: AlertManagerCortexConfig = alertmanagerConfigMock) =>
http.get('/api/alertmanager/grafana/config/api/v1/alerts', () => HttpResponse.json(config));
export const getAlertmanagerConfigHandler = (responseOverride?: StrictResponse<JsonBodyType>) =>
http.get<{ name: string }>('/api/alertmanager/:name/config/api/v1/alerts', ({ params }) => {
if (responseOverride) {
return responseOverride;
}
const { name: alertmanagerName } = params;
export const getAlertmanagerConfigHandler = (config: AlertManagerCortexConfig = alertmanagerConfigMock) =>
http.get('/api/alertmanager/:name/config/api/v1/alerts', () => HttpResponse.json(config));
const configToReturn = getAlertmanagerConfig(alertmanagerName);
if (configToReturn) {
return HttpResponse.json(configToReturn);
}
return HttpResponse.json({ message: 'Not found.' }, { status: 404 });
});
const getAlertmanagerStatusHandler = () =>
http.get<{ name: string }>('/api/alertmanager/:name/api/v2/status', ({ params }) => {
const { name: alertmanagerName } = params;
const statusToReturn = getAlertmanagerStatus(alertmanagerName);
if (statusToReturn) {
return HttpResponse.json(statusToReturn);
}
return HttpResponse.json({ message: 'data source not found', traceID: '' }, { status: 404 });
});
export const ALERTMANAGER_UPDATE_ERROR_RESPONSE = HttpResponse.json({ message: 'bad request' }, { status: 400 });
@ -92,20 +117,20 @@ const validateGrafanaAlertmanagerConfig = (config: AlertManagerCortexConfig) =>
return null;
};
export const updateGrafanaAlertmanagerConfigHandler = (responseOverride?: typeof ALERTMANAGER_UPDATE_ERROR_RESPONSE) =>
http.post('/api/alertmanager/grafana/config/api/v1/alerts', async ({ request }) => {
export const updateAlertmanagerConfigHandler = (responseOverride?: typeof ALERTMANAGER_UPDATE_ERROR_RESPONSE) =>
http.post<{ name: string }>('/api/alertmanager/:name/config/api/v1/alerts', async ({ request, params }) => {
if (responseOverride) {
return responseOverride;
}
const { name: alertmanagerName } = params;
const body: AlertManagerCortexConfig = await request.clone().json();
// TODO: Validate the config depending on alertmanager type
// e.g. validate other AMs differently where required for tests
const potentialError = validateGrafanaAlertmanagerConfig(body);
return potentialError ? potentialError : HttpResponse.json({ message: 'configuration created' });
});
const updateAlertmanagerConfigHandler = () =>
http.post('/api/alertmanager/:name/config/api/v1/alerts', async ({ request }) => {
const body: AlertManagerCortexConfig = await request.clone().json();
const potentialError = validateGrafanaAlertmanagerConfig(body);
if (!potentialError) {
// Only update the mock entity the endpoint is going to "succeed"
setAlertmanagerConfig(alertmanagerName, body);
}
return potentialError ? potentialError : HttpResponse.json({ message: 'configuration created' });
});
@ -140,13 +165,12 @@ const getGroupsHandler = () =>
const handlers = [
alertmanagerAlertsListHandler(),
grafanaAlertingConfigurationStatusHandler(),
getGrafanaAlertmanagerConfigHandler(),
getAlertmanagerConfigHandler(),
updateGrafanaAlertmanagerConfigHandler(),
updateAlertmanagerConfigHandler(),
getGrafanaAlertmanagerTemplatePreview(),
getReceiversHandler(),
testReceiversHandler(),
getGroupsHandler(),
getAlertmanagerStatusHandler(),
];
export default handlers;

View File

@ -1,15 +1,32 @@
import { http, HttpResponse } from 'msw';
import { buildInfoResponse } from 'app/features/alerting/unified/testSetup/featureDiscovery';
/** UID of the alertmanager that is expected to be broken in tests */
export const MOCK_DATASOURCE_UID_BROKEN_ALERTMANAGER = 'FwkfQfEmYlAthB';
/** Display name of the alertmanager that is expected to be broken in tests */
export const MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER = 'broken alertmanager';
export const MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID = 'vanilla-alertmanager';
export const MOCK_DATASOURCE_PROVISIONED_MIMIR_ALERTMANAGER_UID = 'provisioned-alertmanager';
export const MOCK_DATASOURCE_GRAFANA_MIMIR = 'grafana-mimir';
const isSupportedType = (uid: string): uid is keyof typeof buildInfoResponse => {
return uid in buildInfoResponse;
};
// TODO: Add more accurate endpoint responses as tests require
export const datasourceBuildInfoHandler = () =>
http.get('/api/datasources/proxy/uid/:datasourceUid/api/v1/status/buildinfo', () => HttpResponse.json({}));
http.get<{ datasourceUid: keyof typeof buildInfoResponse | string }>(
'/api/datasources/proxy/uid/:datasourceUid/api/v1/status/buildinfo',
({ params }) => {
const { datasourceUid } = params;
if (isSupportedType(datasourceUid)) {
const response = buildInfoResponse[datasourceUid];
return HttpResponse.json(response);
}
return HttpResponse.json({});
}
);
const datasourcesHandlers = [datasourceBuildInfoHandler()];
export default datasourcesHandlers;

View File

@ -1,13 +1,13 @@
import { camelCase } from 'lodash';
import { HttpResponse, http } from 'msw';
import alertmanagerConfig from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { PROVENANCE_NONE, K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
const config: AlertManagerCortexConfig = alertmanagerConfig;
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
// Turn our mock alertmanager config into the format that we expect to be returned by the k8s API
const mappedReceivers =

View File

@ -1,12 +1,12 @@
import { HttpResponse, http } from 'msw';
import alertmanagerConfig from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup } from 'app/features/alerting/unified/openapi/templatesApi.gen';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
const config: AlertManagerCortexConfig = alertmanagerConfig;
const config = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
// Map alertmanager templates to k8s templates
const mappedTemplates = Object.entries(

View File

@ -1,11 +1,11 @@
import { HttpResponse, http } from 'msw';
import alertmanagerConfig from 'app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json';
import { GrafanaManagedContactPoint, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
import { getAlertmanagerConfig } from 'app/features/alerting/unified/mocks/server/entities/alertmanagers';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
const defaultReceiversResponse: GrafanaManagedContactPoint[] = alertmanagerConfig.alertmanager_config.receivers;
const defaultTimeIntervalsResponse: MuteTimeInterval[] = alertmanagerConfig.alertmanager_config.time_intervals;
const alertmanagerConfig = getAlertmanagerConfig(GRAFANA_RULES_SOURCE_NAME);
const defaultReceiversResponse = alertmanagerConfig.alertmanager_config.receivers;
const defaultTimeIntervalsResponse = alertmanagerConfig.alertmanager_config.time_intervals;
const getNotificationReceiversHandler = (response = defaultReceiversResponse) =>
http.get('/api/v1/notifications/receivers', () => HttpResponse.json(response));