receiver-form.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { isArray } from 'lodash';
  2. import {
  3. AlertManagerCortexConfig,
  4. GrafanaManagedReceiverConfig,
  5. Receiver,
  6. Route,
  7. } from 'app/plugins/datasource/alertmanager/types';
  8. import { CloudNotifierType, NotifierDTO, NotifierType } from 'app/types';
  9. import {
  10. CloudChannelConfig,
  11. CloudChannelMap,
  12. CloudChannelValues,
  13. GrafanaChannelMap,
  14. GrafanaChannelValues,
  15. ReceiverFormValues,
  16. } from '../types/receiver-form';
  17. export function grafanaReceiverToFormValues(
  18. receiver: Receiver,
  19. notifiers: NotifierDTO[]
  20. ): [ReceiverFormValues<GrafanaChannelValues>, GrafanaChannelMap] {
  21. const channelMap: GrafanaChannelMap = {};
  22. // giving each form receiver item a unique id so we can use it to map back to "original" items
  23. // as well as to use as `key` prop.
  24. // @TODO use uid once backend is fixed to provide it. then we can get rid of the GrafanaChannelMap
  25. let idCounter = 1;
  26. const values = {
  27. name: receiver.name,
  28. items:
  29. receiver.grafana_managed_receiver_configs?.map((channel) => {
  30. const id = String(idCounter++);
  31. channelMap[id] = channel;
  32. const notifier = notifiers.find(({ type }) => type === channel.type);
  33. return grafanaChannelConfigToFormChannelValues(id, channel, notifier);
  34. }) ?? [],
  35. };
  36. return [values, channelMap];
  37. }
  38. export function cloudReceiverToFormValues(
  39. receiver: Receiver,
  40. notifiers: NotifierDTO[]
  41. ): [ReceiverFormValues<CloudChannelValues>, CloudChannelMap] {
  42. const channelMap: CloudChannelMap = {};
  43. // giving each form receiver item a unique id so we can use it to map back to "original" items
  44. let idCounter = 1;
  45. const items: CloudChannelValues[] = Object.entries(receiver)
  46. // filter out only config items that are relevant to cloud
  47. .filter(([type]) => type.endsWith('_configs') && type !== 'grafana_managed_receiver_configs')
  48. // map property names to cloud notifier types by removing the `_config` suffix
  49. .map(([type, configs]): [CloudNotifierType, CloudChannelConfig[]] => [
  50. type.replace('_configs', '') as CloudNotifierType,
  51. configs as CloudChannelConfig[],
  52. ])
  53. // convert channel configs to form values
  54. .map(([type, configs]) =>
  55. configs.map((config) => {
  56. const id = String(idCounter++);
  57. channelMap[id] = { type, config };
  58. const notifier = notifiers.find((notifier) => notifier.type === type);
  59. if (!notifier) {
  60. throw new Error(`unknown cloud notifier: ${type}`);
  61. }
  62. return cloudChannelConfigToFormChannelValues(id, type, config);
  63. })
  64. )
  65. .flat();
  66. const values = {
  67. name: receiver.name,
  68. items,
  69. };
  70. return [values, channelMap];
  71. }
  72. export function formValuesToGrafanaReceiver(
  73. values: ReceiverFormValues<GrafanaChannelValues>,
  74. channelMap: GrafanaChannelMap,
  75. defaultChannelValues: GrafanaChannelValues
  76. ): Receiver {
  77. return {
  78. name: values.name,
  79. grafana_managed_receiver_configs: (values.items ?? []).map((channelValues) => {
  80. const existing: GrafanaManagedReceiverConfig | undefined = channelMap[channelValues.__id];
  81. return formChannelValuesToGrafanaChannelConfig(channelValues, defaultChannelValues, values.name, existing);
  82. }),
  83. };
  84. }
  85. export function formValuesToCloudReceiver(
  86. values: ReceiverFormValues<CloudChannelValues>,
  87. defaults: CloudChannelValues
  88. ): Receiver {
  89. const recv: Receiver = {
  90. name: values.name,
  91. };
  92. values.items.forEach(({ __id, type, settings, sendResolved }) => {
  93. const channel = omitEmptyValues({
  94. ...settings,
  95. send_resolved: sendResolved ?? defaults.sendResolved,
  96. });
  97. const configsKey = `${type}_configs`;
  98. if (!recv[configsKey]) {
  99. recv[configsKey] = [channel];
  100. } else {
  101. (recv[configsKey] as unknown[]).push(channel);
  102. }
  103. });
  104. return recv;
  105. }
  106. // will add new receiver, or replace exisitng one
  107. export function updateConfigWithReceiver(
  108. config: AlertManagerCortexConfig,
  109. receiver: Receiver,
  110. replaceReceiverName?: string
  111. ): AlertManagerCortexConfig {
  112. const oldReceivers = config.alertmanager_config.receivers ?? [];
  113. // sanity check that name is not duplicated
  114. if (receiver.name !== replaceReceiverName && !!oldReceivers.find(({ name }) => name === receiver.name)) {
  115. throw new Error(`Duplicate receiver name ${receiver.name}`);
  116. }
  117. // sanity check that existing receiver exists
  118. if (replaceReceiverName && !oldReceivers.find(({ name }) => name === replaceReceiverName)) {
  119. throw new Error(`Expected receiver ${replaceReceiverName} to exist, but did not find it in the config`);
  120. }
  121. const updated: AlertManagerCortexConfig = {
  122. ...config,
  123. alertmanager_config: {
  124. // @todo rename receiver on routes as necessary
  125. ...config.alertmanager_config,
  126. receivers: replaceReceiverName
  127. ? oldReceivers.map((existingReceiver) =>
  128. existingReceiver.name === replaceReceiverName ? receiver : existingReceiver
  129. )
  130. : [...oldReceivers, receiver],
  131. },
  132. };
  133. // if receiver was renamed, rename it in routes as well
  134. if (updated.alertmanager_config.route && replaceReceiverName && receiver.name !== replaceReceiverName) {
  135. updated.alertmanager_config.route = renameReceiverInRoute(
  136. updated.alertmanager_config.route,
  137. replaceReceiverName,
  138. receiver.name
  139. );
  140. }
  141. return updated;
  142. }
  143. function renameReceiverInRoute(route: Route, oldName: string, newName: string) {
  144. const updated: Route = {
  145. ...route,
  146. };
  147. if (updated.receiver === oldName) {
  148. updated.receiver = newName;
  149. }
  150. if (updated.routes) {
  151. updated.routes = updated.routes.map((route) => renameReceiverInRoute(route, oldName, newName));
  152. }
  153. return updated;
  154. }
  155. function cloudChannelConfigToFormChannelValues(
  156. id: string,
  157. type: CloudNotifierType,
  158. channel: CloudChannelConfig
  159. ): CloudChannelValues {
  160. return {
  161. __id: id,
  162. type,
  163. settings: {
  164. ...channel,
  165. },
  166. secureFields: {},
  167. secureSettings: {},
  168. sendResolved: channel.send_resolved,
  169. };
  170. }
  171. function grafanaChannelConfigToFormChannelValues(
  172. id: string,
  173. channel: GrafanaManagedReceiverConfig,
  174. notifier?: NotifierDTO
  175. ): GrafanaChannelValues {
  176. const values: GrafanaChannelValues = {
  177. __id: id,
  178. type: channel.type as NotifierType,
  179. secureSettings: {},
  180. settings: { ...channel.settings },
  181. secureFields: { ...channel.secureFields },
  182. disableResolveMessage: channel.disableResolveMessage,
  183. };
  184. // work around https://github.com/grafana/alerting-squad/issues/100
  185. notifier?.options.forEach((option) => {
  186. if (option.secure && values.settings[option.propertyName]) {
  187. delete values.settings[option.propertyName];
  188. values.secureFields[option.propertyName] = true;
  189. }
  190. });
  191. return values;
  192. }
  193. export function formChannelValuesToGrafanaChannelConfig(
  194. values: GrafanaChannelValues,
  195. defaults: GrafanaChannelValues,
  196. name: string,
  197. existing?: GrafanaManagedReceiverConfig
  198. ): GrafanaManagedReceiverConfig {
  199. const channel: GrafanaManagedReceiverConfig = {
  200. settings: omitEmptyValues({
  201. ...(existing && existing.type === values.type ? existing.settings ?? {} : {}),
  202. ...(values.settings ?? {}),
  203. }),
  204. secureSettings: values.secureSettings ?? {},
  205. type: values.type,
  206. name,
  207. disableResolveMessage:
  208. values.disableResolveMessage ?? existing?.disableResolveMessage ?? defaults.disableResolveMessage,
  209. };
  210. if (existing) {
  211. channel.uid = existing.uid;
  212. }
  213. return channel;
  214. }
  215. // will remove properties that have empty ('', null, undefined) object properties.
  216. // traverses nested objects and arrays as well. in place, mutates the object.
  217. // this is needed because form will submit empty string for not filled in fields,
  218. // but for cloud alertmanager receiver config to use global default value the property must be omitted entirely
  219. // this isn't a perfect solution though. No way for user to intentionally provide an empty string. Will need rethinking later
  220. export function omitEmptyValues<T>(obj: T): T {
  221. if (isArray(obj)) {
  222. obj.forEach(omitEmptyValues);
  223. } else if (typeof obj === 'object' && obj !== null) {
  224. Object.entries(obj).forEach(([key, value]) => {
  225. if (value === '' || value === null || value === undefined) {
  226. delete (obj as any)[key];
  227. } else {
  228. omitEmptyValues(value);
  229. }
  230. });
  231. }
  232. return obj;
  233. }