The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/unified/utils/receiver-form.ts

253 lines
8.1 KiB

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<GrafanaChannelValues>, 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<CloudChannelValues>, 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<GrafanaChannelValues>,
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<CloudChannelValues>,
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<T>(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;
}