import { isArray } from 'lodash'; import { AlertManagerCortexConfig, GrafanaManagedReceiverConfig, Receiver, Route, } from 'app/plugins/datasource/alertmanager/types'; import { CloudNotifierType, NotifierDTO, NotifierType } from 'app/types'; import { CloudChannelConfig, CloudChannelMap, CloudChannelValues, GrafanaChannelMap, GrafanaChannelValues, ReceiverFormValues, } from '../types/receiver-form'; export function grafanaReceiverToFormValues( receiver: Receiver, notifiers: NotifierDTO[] ): [ReceiverFormValues, GrafanaChannelMap] { const channelMap: GrafanaChannelMap = {}; // giving each form receiver item a unique id so we can use it to map back to "original" items // as well as to use as `key` prop. // @TODO use uid once backend is fixed to provide it. then we can get rid of the GrafanaChannelMap let idCounter = 1; const values = { name: receiver.name, items: receiver.grafana_managed_receiver_configs?.map((channel) => { const id = String(idCounter++); channelMap[id] = channel; const notifier = notifiers.find(({ type }) => type === channel.type); return grafanaChannelConfigToFormChannelValues(id, channel, notifier); }) ?? [], }; return [values, channelMap]; } export function cloudReceiverToFormValues( receiver: Receiver, notifiers: NotifierDTO[] ): [ReceiverFormValues, CloudChannelMap] { const channelMap: CloudChannelMap = {}; // giving each form receiver item a unique id so we can use it to map back to "original" items let idCounter = 1; const items: CloudChannelValues[] = Object.entries(receiver) // filter out only config items that are relevant to cloud .filter(([type]) => type.endsWith('_configs') && type !== 'grafana_managed_receiver_configs') // map property names to cloud notifier types by removing the `_config` suffix .map(([type, configs]): [CloudNotifierType, CloudChannelConfig[]] => [ type.replace('_configs', '') as CloudNotifierType, configs as CloudChannelConfig[], ]) // convert channel configs to form values .map(([type, configs]) => configs.map((config) => { const id = String(idCounter++); channelMap[id] = { type, config }; const notifier = notifiers.find((notifier) => notifier.type === type); if (!notifier) { throw new Error(`unknown cloud notifier: ${type}`); } return cloudChannelConfigToFormChannelValues(id, type, config); }) ) .flat(); const values = { name: receiver.name, items, }; return [values, channelMap]; } export function formValuesToGrafanaReceiver( values: ReceiverFormValues, channelMap: GrafanaChannelMap, defaultChannelValues: GrafanaChannelValues ): Receiver { return { name: values.name, grafana_managed_receiver_configs: (values.items ?? []).map((channelValues) => { const existing: GrafanaManagedReceiverConfig | undefined = channelMap[channelValues.__id]; return formChannelValuesToGrafanaChannelConfig(channelValues, defaultChannelValues, values.name, existing); }), }; } export function formValuesToCloudReceiver( values: ReceiverFormValues, defaults: CloudChannelValues ): Receiver { const recv: Receiver = { name: values.name, }; values.items.forEach(({ __id, type, settings, sendResolved }) => { const channel = omitEmptyValues({ ...settings, send_resolved: sendResolved ?? defaults.sendResolved, }); const configsKey = `${type}_configs`; if (!recv[configsKey]) { recv[configsKey] = [channel]; } else { (recv[configsKey] as unknown[]).push(channel); } }); return recv; } // will add new receiver, or replace exisitng one export function updateConfigWithReceiver( config: AlertManagerCortexConfig, receiver: Receiver, replaceReceiverName?: string ): AlertManagerCortexConfig { const oldReceivers = config.alertmanager_config.receivers ?? []; // sanity check that name is not duplicated if (receiver.name !== replaceReceiverName && !!oldReceivers.find(({ name }) => name === receiver.name)) { throw new Error(`Duplicate receiver name ${receiver.name}`); } // sanity check that existing receiver exists if (replaceReceiverName && !oldReceivers.find(({ name }) => name === replaceReceiverName)) { throw new Error(`Expected receiver ${replaceReceiverName} to exist, but did not find it in the config`); } const updated: AlertManagerCortexConfig = { ...config, alertmanager_config: { // @todo rename receiver on routes as necessary ...config.alertmanager_config, receivers: replaceReceiverName ? oldReceivers.map((existingReceiver) => existingReceiver.name === replaceReceiverName ? receiver : existingReceiver ) : [...oldReceivers, receiver], }, }; // if receiver was renamed, rename it in routes as well if (updated.alertmanager_config.route && replaceReceiverName && receiver.name !== replaceReceiverName) { updated.alertmanager_config.route = renameReceiverInRoute( updated.alertmanager_config.route, replaceReceiverName, receiver.name ); } return updated; } function renameReceiverInRoute(route: Route, oldName: string, newName: string) { const updated: Route = { ...route, }; if (updated.receiver === oldName) { updated.receiver = newName; } if (updated.routes) { updated.routes = updated.routes.map((route) => renameReceiverInRoute(route, oldName, newName)); } return updated; } function cloudChannelConfigToFormChannelValues( id: string, type: CloudNotifierType, channel: CloudChannelConfig ): CloudChannelValues { return { __id: id, type, settings: { ...channel, }, secureFields: {}, secureSettings: {}, sendResolved: channel.send_resolved, }; } function grafanaChannelConfigToFormChannelValues( id: string, channel: GrafanaManagedReceiverConfig, notifier?: NotifierDTO ): GrafanaChannelValues { const values: GrafanaChannelValues = { __id: id, type: channel.type as NotifierType, secureSettings: {}, settings: { ...channel.settings }, secureFields: { ...channel.secureFields }, disableResolveMessage: channel.disableResolveMessage, }; // work around https://github.com/grafana/alerting-squad/issues/100 notifier?.options.forEach((option) => { if (option.secure && values.settings[option.propertyName]) { delete values.settings[option.propertyName]; values.secureFields[option.propertyName] = true; } }); return values; } export function formChannelValuesToGrafanaChannelConfig( values: GrafanaChannelValues, defaults: GrafanaChannelValues, name: string, existing?: GrafanaManagedReceiverConfig ): GrafanaManagedReceiverConfig { const channel: GrafanaManagedReceiverConfig = { settings: omitEmptyValues({ ...(existing && existing.type === values.type ? existing.settings ?? {} : {}), ...(values.settings ?? {}), }), secureSettings: values.secureSettings ?? {}, type: values.type, name, disableResolveMessage: values.disableResolveMessage ?? existing?.disableResolveMessage ?? defaults.disableResolveMessage, }; if (existing) { channel.uid = existing.uid; } return channel; } // will remove properties that have empty ('', null, undefined) object properties. // traverses nested objects and arrays as well. in place, mutates the object. // this is needed because form will submit empty string for not filled in fields, // but for cloud alertmanager receiver config to use global default value the property must be omitted entirely // this isn't a perfect solution though. No way for user to intentionally provide an empty string. Will need rethinking later export function omitEmptyValues(obj: T): T { if (isArray(obj)) { obj.forEach(omitEmptyValues); } else if (typeof obj === 'object' && obj !== null) { Object.entries(obj).forEach(([key, value]) => { if (value === '' || value === null || value === undefined) { delete (obj as any)[key]; } else { omitEmptyValues(value); } }); } return obj; }