Alerting: useProduceNewAlertmanagerConfiguration for notification templates (#95290)

This commit is contained in:
Gilles De Mey 2024-11-13 11:46:10 +01:00 committed by GitHub
parent 33b4c71cb2
commit f0c57622f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 304 additions and 98 deletions

View File

@ -1,17 +1,20 @@
import { produce } from 'immer';
import { useEffect } from 'react';
import { Validate } from 'react-hook-form';
import { useDispatch } from 'app/types';
import { AlertManagerCortexConfig } from '../../../../../plugins/datasource/alertmanager/types';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { templatesApi } from '../../api/templateApi';
import { useAsync } from '../../hooks/useAsync';
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup,
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroupList,
} from '../../openapi/templatesApi.gen';
import { updateAlertManagerConfigAction } from '../../state/actions';
import {
addNotificationTemplateAction,
deleteNotificationTemplateAction,
updateNotificationTemplateAction,
} from '../../reducers/alertmanager/notificationTemplates';
import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants';
import { getAnnotation, getK8sNamespace, shouldUseK8sApi } from '../../utils/k8s/utils';
import { ensureDefine } from '../../utils/templates';
@ -30,6 +33,7 @@ export interface NotificationTemplate {
}
const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi;
const {
useListNamespacedTemplateGroupQuery,
useLazyReadNamespacedTemplateGroupQuery,
@ -146,39 +150,17 @@ interface CreateTemplateParams {
}
export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) {
const dispatch = useDispatch();
const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery();
const [createNamespacedTemplateGroup] = useCreateNamespacedTemplateGroupMutation();
const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
const k8sApiSupported = shouldUseK8sApi(alertmanager);
async function createUsingConfigFileApi({ templateValues }: CreateTemplateParams) {
const amConfig = await fetchAmConfig(alertmanager).unwrap();
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(templateValues.title, templateValues.content);
const createUsingConfigFileApi = useAsync(({ templateValues }: CreateTemplateParams) => {
const action = addNotificationTemplateAction({ template: templateValues });
return updateAlertmanagerConfiguration(action);
});
const targetTemplateExists = amConfig.template_files?.[templateValues.title] !== undefined;
if (targetTemplateExists) {
throw new Error('target template already exists');
}
const updatedConfig = produce(amConfig, (draft) => {
draft.template_files[templateValues.title] = content;
draft.alertmanager_config.templates = [...(draft.alertmanager_config.templates ?? []), templateValues.title];
});
return dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName: alertmanager,
newConfig: updatedConfig,
oldConfig: amConfig,
})
).unwrap();
}
async function createUsingK8sApi({ templateValues }: CreateTemplateParams) {
const createUsingK8sApi = useAsync(({ templateValues }: CreateTemplateParams) => {
const content = ensureDefine(templateValues.title, templateValues.content);
return createNamespacedTemplateGroup({
@ -188,7 +170,7 @@ export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanager
metadata: {},
},
}).unwrap();
}
});
return k8sApiSupported ? createUsingK8sApi : createUsingConfigFileApi;
}
@ -199,43 +181,17 @@ interface UpdateTemplateParams {
}
export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) {
const dispatch = useDispatch();
const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery();
const [replaceNamespacedTemplateGroup] = useReplaceNamespacedTemplateGroupMutation();
const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
const k8sApiSupported = shouldUseK8sApi(alertmanager);
async function updateUsingConfigFileApi({ template, patch }: UpdateTemplateParams) {
const amConfig = await fetchAmConfig(alertmanager).unwrap();
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(patch.title, patch.content);
const updateUsingConfigFileApi = useAsync(({ template, patch }: UpdateTemplateParams) => {
const action = updateNotificationTemplateAction({ name: template.title, template: patch });
return updateAlertmanagerConfiguration(action);
});
const originalName = template.title; // For ConfigFile API name is the same as uid
const nameChanged = originalName !== patch.title;
// TODO Maybe we could simplify or extract this logic
const updatedConfig = produce(amConfig, (draft) => {
if (nameChanged) {
delete draft.template_files[originalName];
draft.alertmanager_config.templates = draft.alertmanager_config.templates?.filter((t) => t !== originalName);
}
draft.template_files[patch.title] = content;
draft.alertmanager_config.templates = [...(draft.alertmanager_config.templates ?? []), patch.title];
});
return dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName: alertmanager,
newConfig: updatedConfig,
oldConfig: amConfig,
})
).unwrap();
}
async function updateUsingK8sApi({ template, patch }: UpdateTemplateParams) {
const updateUsingK8sApi = useAsync(({ template, patch }: UpdateTemplateParams) => {
const content = ensureDefine(patch.title, patch.content);
return replaceNamespacedTemplateGroup({
@ -246,43 +202,30 @@ export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanager
metadata: { name: template.uid },
},
}).unwrap();
}
});
return k8sApiSupported ? updateUsingK8sApi : updateUsingConfigFileApi;
}
export function useDeleteNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) {
const dispatch = useDispatch();
const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery();
const [deleteNamespacedTemplateGroup] = useDeleteNamespacedTemplateGroupMutation();
const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
async function deleteUsingConfigFileApi({ uid }: { uid: string }) {
const amConfig = await fetchAmConfig(alertmanager).unwrap();
const deleteUsingConfigAPI = useAsync(async ({ uid }: { uid: string }) => {
const action = deleteNotificationTemplateAction({ name: uid });
return updateAlertmanagerConfiguration(action);
});
const updatedConfig = produce(amConfig, (draft) => {
delete draft.template_files[uid];
draft.alertmanager_config.templates = draft.alertmanager_config.templates?.filter((t) => t !== uid);
});
return dispatch(
updateAlertManagerConfigAction({
alertManagerSourceName: alertmanager,
newConfig: updatedConfig,
oldConfig: amConfig,
})
).unwrap();
}
async function deleteUsingK8sApi({ uid }: { uid: string }) {
const deleteUsingK8sApi = useAsync(({ uid }: { uid: string }) => {
return deleteNamespacedTemplateGroup({
namespace: getK8sNamespace(),
name: uid,
ioK8SApimachineryPkgApisMetaV1DeleteOptions: {},
}).unwrap();
}
});
const k8sApiSupported = shouldUseK8sApi(alertmanager);
return k8sApiSupported ? deleteUsingK8sApi : deleteUsingConfigFileApi;
return k8sApiSupported ? deleteUsingK8sApi : deleteUsingConfigAPI;
}
interface ValidateNotificationTemplateParams {

View File

@ -10,18 +10,18 @@ import { GrafanaTheme2 } from '@grafana/data';
import { isFetchError, locationService } from '@grafana/runtime';
import {
Alert,
Box,
Button,
Drawer,
Dropdown,
FieldSet,
InlineField,
Input,
LinkButton,
Menu,
useStyles2,
Stack,
useSplitter,
Drawer,
InlineField,
Box,
useStyles2,
} from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup';
@ -93,8 +93,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
const appNotification = useAppNotification();
const createNewTemplate = useCreateNotificationTemplate({ alertmanager });
const updateTemplate = useUpdateNotificationTemplate({ alertmanager });
const [createNewTemplate] = useCreateNotificationTemplate({ alertmanager });
const [updateTemplate] = useUpdateNotificationTemplate({ alertmanager });
const { titleIsUnique } = useValidateNotificationTemplate({ alertmanager, originalTemplate });
useCleanup((state) => (state.unifiedAlerting.saveAMConfig = initialAsyncRequestState));
@ -149,9 +149,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
try {
if (!originalTemplate) {
await createNewTemplate({ templateValues: values });
await createNewTemplate.execute({ templateValues: values });
} else {
await updateTemplate({ template: originalTemplate, patch: values });
await updateTemplate.execute({ template: originalTemplate, patch: values });
}
appNotification.success('Template saved', `Template ${values.title} has been saved`);
locationService.push(returnLink);

View File

@ -30,7 +30,7 @@ interface Props {
export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
const appNotification = useAppNotification();
const deleteTemplate = useDeleteNotificationTemplate({ alertmanager: alertManagerName });
const [deleteTemplate] = useDeleteNotificationTemplate({ alertmanager: alertManagerName });
const tableStyles = useStyles2(getAlertTableStyles);
@ -39,7 +39,7 @@ export const TemplatesTable = ({ alertManagerName, templates }: Props) => {
const onDeleteTemplate = async () => {
if (templateToDelete) {
try {
await deleteTemplate({ uid: templateToDelete.uid });
await deleteTemplate.execute({ uid: templateToDelete.uid });
appNotification.success('Template deleted', `Template ${templateToDelete.title} has been deleted`);
} catch (error) {
appNotification.error('Error deleting template', `Error deleting template ${templateToDelete.title}`);

View File

@ -5,6 +5,7 @@ import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/ty
import { alertmanagerApi } from '../api/alertmanagerApi';
import { muteTimingsReducer } from '../reducers/alertmanager/muteTimings';
import { notificationTemplatesReducer } from '../reducers/alertmanager/notificationTemplates';
import { receiversReducer } from '../reducers/alertmanager/receivers';
import { useAlertmanager } from '../state/AlertmanagerContext';
@ -26,7 +27,12 @@ export const initialAlertmanagerConfiguration: AlertManagerCortexConfig = {
template_files: {},
};
const configurationReducer = reduceReducers(initialAlertmanagerConfiguration, muteTimingsReducer, receiversReducer);
const configurationReducer = reduceReducers(
initialAlertmanagerConfiguration,
muteTimingsReducer,
receiversReducer,
notificationTemplatesReducer
);
/**
* This hook will make sure we are always applying actions that mutate the Alertmanager configuration

View File

@ -0,0 +1,55 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`notification templates should add a new notification template 1`] = `
{
"alertmanager_config": {
"templates": [
"foo",
],
},
"template_files": {
"foo": "{{ define "foo" }}
foo
{{ end }}",
},
}
`;
exports[`notification templates should allow renaming a notification template 1`] = `
{
"alertmanager_config": {
"templates": [
"rename-me-copy",
],
},
"template_files": {
"rename-me-copy": "{{ define "rename-me-copy" }}
rename me, please
{{ end }}",
},
}
`;
exports[`notification templates should remove a notification template 1`] = `
{
"alertmanager_config": {
"templates": [],
},
"template_files": {},
}
`;
exports[`notification templates should update a notification template without renaming 1`] = `
{
"alertmanager_config": {
"templates": [
"update-me",
],
},
"template_files": {
"update-me": "{{ define "update-me" }}
update me, please
{{ end }}",
},
}
`;

View File

@ -0,0 +1,119 @@
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { TemplateFormValues } from '../../components/receivers/TemplateForm';
import {
addNotificationTemplateAction,
deleteNotificationTemplateAction,
notificationTemplatesReducer,
updateNotificationTemplateAction,
} from './notificationTemplates';
describe('notification templates', () => {
it('should add a new notification template', () => {
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: {},
template_files: {},
};
const newNotificationTemplate: TemplateFormValues = {
title: 'foo',
content: 'foo',
};
const action = addNotificationTemplateAction({ template: newNotificationTemplate });
expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot();
});
it('should not add a new notification template if the name already exists', () => {
const name = 'existing';
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: { templates: [name] },
template_files: {
[name]: 'foo',
},
};
const newNotificationTemplate: TemplateFormValues = {
title: name,
content: 'foo',
};
const action = addNotificationTemplateAction({ template: newNotificationTemplate });
expect(() => notificationTemplatesReducer(initialConfig, action)).toThrow(/already exists/);
});
it('should update a notification template without renaming', () => {
const name = 'update-me';
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: {
templates: [name],
},
template_files: {
[name]: 'update me',
},
};
const action = updateNotificationTemplateAction({ name, template: { title: name, content: 'update me, please' } });
expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot();
});
it('should not update if target does not exist', () => {
const name = 'update-me';
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: {},
template_files: {},
};
const action = updateNotificationTemplateAction({ name, template: { title: name, content: 'update me, please' } });
expect(() => notificationTemplatesReducer(initialConfig, action)).toThrow(/did not find it/);
});
it('should not update if renaming and new template name exist', () => {
const name = 'rename-me';
const name2 = 'rename-me-2';
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: {
templates: [name, name2],
},
template_files: {
[name]: 'foo',
[name2]: 'bar',
},
};
const action = updateNotificationTemplateAction({ name, template: { title: name2, content: 'foo' } });
expect(() => notificationTemplatesReducer(initialConfig, action)).toThrow(/duplicate/i);
});
it('should allow renaming a notification template', () => {
const name = 'rename-me';
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: {
templates: [name],
},
template_files: {
[name]: 'rename me',
},
};
const action = updateNotificationTemplateAction({
name,
template: { title: 'rename-me-copy', content: 'rename me, please' },
});
expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot();
});
it('should remove a notification template', () => {
const name = 'delete-me';
const initialConfig: AlertManagerCortexConfig = {
alertmanager_config: {
templates: [name],
},
template_files: {
[name]: 'delete me please',
},
};
const action = deleteNotificationTemplateAction({ name });
expect(notificationTemplatesReducer(initialConfig, action)).toMatchSnapshot();
});
});

View File

@ -0,0 +1,83 @@
import { createAction, createReducer } from '@reduxjs/toolkit';
import { remove, toArray, unset } from 'lodash';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { TemplateFormValues } from '../../components/receivers/TemplateForm';
import { ensureDefine } from '../../utils/templates';
export const addNotificationTemplateAction = createAction<{ template: TemplateFormValues }>('notificationTemplate/add');
export const updateNotificationTemplateAction = createAction<{
name: string;
template: TemplateFormValues;
}>('notificationTemplate/update');
export const deleteNotificationTemplateAction = createAction<{ name: string }>('notificationTemplate/delete');
const initialState: AlertManagerCortexConfig = {
alertmanager_config: {},
template_files: {},
};
/**
* This reducer will manage action related to notification templates and make sure all operations on the alertmanager
* configuration happen immutably and only mutate what they need.
*/
export const notificationTemplatesReducer = createReducer(initialState, (builder) => {
builder
.addCase(addNotificationTemplateAction, (draft, { payload }) => {
const { alertmanager_config = {}, template_files = {} } = draft;
const { template } = payload;
const targetTemplateExists = template_files[template.title] !== undefined;
if (targetTemplateExists) {
throw new Error('target template already exists');
}
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(template.title, template.content);
// add the template to the list of template files
template_files[template.title] = content;
// add the template to the alertmanager_config
alertmanager_config.templates = toArray(alertmanager_config.templates).concat(template.title);
})
.addCase(updateNotificationTemplateAction, (draft, { payload }) => {
const { alertmanager_config = {}, template_files = {} } = draft;
const { name, template } = payload;
const renaming = name !== template.title;
const targetExists = template_files[name] !== undefined;
if (!targetExists) {
throw new Error(`Expected notification template ${name} to exist, but did not find it in the config`);
}
// wrap content in "define" if it's not already wrapped, in case user did not do it/
// it's not obvious that this is needed for template to work
const content = ensureDefine(template.title, template.content);
if (renaming) {
const oldName = name;
const newName = template.title;
const targetExists = template_files[newName] !== undefined;
if (targetExists) {
throw new Error(`Duplicate template name ${newName}`);
}
unset(template_files, oldName);
remove(alertmanager_config.templates ?? [], (templateName) => templateName === oldName);
alertmanager_config.templates = toArray(alertmanager_config.templates).concat(template.title);
}
template_files[template.title] = content;
})
.addCase(deleteNotificationTemplateAction, (draft, { payload }) => {
const { name } = payload;
const { alertmanager_config = {}, template_files = {} } = draft;
unset(template_files, name);
remove(alertmanager_config.templates ?? [], (templateName) => templateName === name);
});
});