import { useEffect } from 'react'; import { Validate } from 'react-hook-form'; import { getAPINamespace } from '../../../../../api/utils'; 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 { addNotificationTemplateAction, deleteNotificationTemplateAction, updateNotificationTemplateAction, } from '../../reducers/alertmanager/notificationTemplates'; import { K8sAnnotations, PROVENANCE_NONE } from '../../utils/k8s/constants'; import { getAnnotation, shouldUseK8sApi } from '../../utils/k8s/utils'; import { ensureDefine } from '../../utils/templates'; import { TemplateFormValues } from '../receivers/TemplateForm'; interface BaseAlertmanagerArgs { alertmanager: string; } export interface NotificationTemplate { uid: string; title: string; content: string; provenance: string; missing?: boolean; } const { useGetAlertmanagerConfigurationQuery, useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; const { useListNamespacedTemplateGroupQuery, useLazyReadNamespacedTemplateGroupQuery, useCreateNamespacedTemplateGroupMutation, useReplaceNamespacedTemplateGroupMutation, useDeleteNamespacedTemplateGroupMutation, } = templatesApi; export function useNotificationTemplates({ alertmanager }: BaseAlertmanagerArgs) { const k8sApiSupported = shouldUseK8sApi(alertmanager); const k8sApiTemplatesRequestState = useListNamespacedTemplateGroupQuery( { namespace: getAPINamespace() }, { skip: !k8sApiSupported, selectFromResult: (state) => ({ ...state, data: state.data ? templateGroupsToTemplates(state.data) : undefined, currentData: state.currentData ? templateGroupsToTemplates(state.currentData) : undefined, }), } ); const configApiTemplatesRequestState = useGetAlertmanagerConfigurationQuery(alertmanager, { skip: k8sApiSupported, selectFromResult: (state) => ({ ...state, data: state.data ? amConfigToTemplates(state.data) : undefined, currentData: state.currentData ? amConfigToTemplates(state.currentData) : undefined, }), }); return k8sApiSupported ? k8sApiTemplatesRequestState : configApiTemplatesRequestState; } function templateGroupsToTemplates( templateGroups: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroupList ): NotificationTemplate[] { return templateGroups.items.map((templateGroup) => templateGroupToTemplate(templateGroup)); } function templateGroupToTemplate( templateGroup: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup ): NotificationTemplate { const provenance = getAnnotation(templateGroup, K8sAnnotations.Provenance) ?? PROVENANCE_NONE; return { // K8s entities should always have a metadata.name property. The type is marked as optional because it's also used in other places uid: templateGroup.metadata.name ?? templateGroup.spec.title, title: templateGroup.spec.title, content: templateGroup.spec.content, provenance, }; } function amConfigToTemplates(config: AlertManagerCortexConfig): NotificationTemplate[] { const { alertmanager_config } = config; const { templates = [] } = alertmanager_config; return Object.entries(config.template_files).map(([title, content]) => ({ uid: title, title, content, // Undefined, null or empty string should be converted to PROVENANCE_NONE provenance: (config.template_file_provenances ?? {})[title] || PROVENANCE_NONE, missing: !templates.includes(title), })); } interface GetTemplateParams extends BaseAlertmanagerArgs { uid: string; } export function useGetNotificationTemplate({ alertmanager, uid }: GetTemplateParams) { const [fetchAmConfig, amConfigStatus] = useLazyGetAlertmanagerConfigurationQuery({ selectFromResult: (state) => ({ ...state, data: state.data ? amConfigToTemplate(state.data, uid) : undefined, currentData: state.currentData ? amConfigToTemplate(state.currentData, uid) : undefined, // TODO set error and isError in case template is not found }), }); const [fetchTemplate, templateStatus] = useLazyReadNamespacedTemplateGroupQuery({ selectFromResult: (state) => { return { ...state, data: state.data ? templateGroupToTemplate(state.data) : undefined, currentData: state.currentData ? templateGroupToTemplate(state.currentData) : undefined, }; }, }); const k8sApiSupported = shouldUseK8sApi(alertmanager); // TODO: Decide on a consistent approach for conditionally calling in our hooks - // useEffect? or using `skip` properly in RTKQ hooks? // What are pros and cons of each? useEffect(() => { if (k8sApiSupported) { fetchTemplate({ namespace: getAPINamespace(), name: uid }); } else { fetchAmConfig(alertmanager); } }, [alertmanager, uid, k8sApiSupported, fetchAmConfig, fetchTemplate]); return k8sApiSupported ? templateStatus : amConfigStatus; } function amConfigToTemplate(config: AlertManagerCortexConfig, name: string): NotificationTemplate | undefined { const templates = amConfigToTemplates(config); return templates.find((t) => t.title === name); } interface CreateTemplateParams { templateValues: TemplateFormValues; } export function useCreateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) { const [createNamespacedTemplateGroup] = useCreateNamespacedTemplateGroupMutation(); const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration(); const k8sApiSupported = shouldUseK8sApi(alertmanager); const createUsingConfigFileApi = useAsync(({ templateValues }: CreateTemplateParams) => { const action = addNotificationTemplateAction({ template: templateValues }); return updateAlertmanagerConfiguration(action); }); const createUsingK8sApi = useAsync(({ templateValues }: CreateTemplateParams) => { const content = ensureDefine(templateValues.title, templateValues.content); return createNamespacedTemplateGroup({ namespace: getAPINamespace(), comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup: { spec: { title: templateValues.title, content }, metadata: {}, }, }).unwrap(); }); return k8sApiSupported ? createUsingK8sApi : createUsingConfigFileApi; } interface UpdateTemplateParams { template: NotificationTemplate; patch: TemplateFormValues; } export function useUpdateNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) { const [replaceNamespacedTemplateGroup] = useReplaceNamespacedTemplateGroupMutation(); const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration(); const k8sApiSupported = shouldUseK8sApi(alertmanager); const updateUsingConfigFileApi = useAsync(({ template, patch }: UpdateTemplateParams) => { const action = updateNotificationTemplateAction({ name: template.title, template: patch }); return updateAlertmanagerConfiguration(action); }); const updateUsingK8sApi = useAsync(({ template, patch }: UpdateTemplateParams) => { const content = ensureDefine(patch.title, patch.content); return replaceNamespacedTemplateGroup({ namespace: getAPINamespace(), name: template.uid, comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TemplateGroup: { spec: { title: patch.title, content }, metadata: { name: template.uid }, }, }).unwrap(); }); return k8sApiSupported ? updateUsingK8sApi : updateUsingConfigFileApi; } export function useDeleteNotificationTemplate({ alertmanager }: BaseAlertmanagerArgs) { const [deleteNamespacedTemplateGroup] = useDeleteNamespacedTemplateGroupMutation(); const [updateAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration(); const deleteUsingConfigAPI = useAsync(async ({ uid }: { uid: string }) => { const action = deleteNotificationTemplateAction({ name: uid }); return updateAlertmanagerConfiguration(action); }); const deleteUsingK8sApi = useAsync(({ uid }: { uid: string }) => { return deleteNamespacedTemplateGroup({ namespace: getAPINamespace(), name: uid, ioK8SApimachineryPkgApisMetaV1DeleteOptions: {}, }).unwrap(); }); const k8sApiSupported = shouldUseK8sApi(alertmanager); return k8sApiSupported ? deleteUsingK8sApi : deleteUsingConfigAPI; } interface ValidateNotificationTemplateParams { alertmanager: string; originalTemplate?: NotificationTemplate; } export function useValidateNotificationTemplate({ alertmanager, originalTemplate, }: ValidateNotificationTemplateParams) { const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi; const [fetchAmConfig] = useLazyGetAlertmanagerConfigurationQuery(); const titleIsUnique: Validate = async (name) => { const k8sApiSupported = shouldUseK8sApi(alertmanager); if (k8sApiSupported) { // K8s API handles validation for us, so we can just return true // and rely on API errors return true; } if (originalTemplate?.title === name) { // If original template is defined we update existing template so name will not be unique but it's ok return true; } const amConfig = await fetchAmConfig(alertmanager).unwrap(); const templates = amConfigToTemplates(amConfig); const templateOfThisNameExists = templates.some((t) => t.title === name); if (templateOfThisNameExists) { return 'Another template with this name already exists'; } return true; }; return { titleIsUnique, }; } interface NotificationTemplateMetadata { isProvisioned: boolean; } export function useNotificationTemplateMetadata( template: NotificationTemplate | undefined ): NotificationTemplateMetadata { if (!template) { return { isProvisioned: false, }; } return { isProvisioned: Boolean(template.provenance) && template.provenance !== PROVENANCE_NONE, }; }