1import { getBackendSrv, locationService } from '@grafana/runtime'; 2import { createAsyncThunk } from '@reduxjs/toolkit'; 3import { 4 AlertmanagerAlert, 5 AlertManagerCortexConfig, 6 AlertmanagerGroup, 7 ExternalAlertmanagersResponse, 8 Receiver, 9 Silence, 10 SilenceCreatePayload, 11 TestReceiversAlert, 12} from 'app/plugins/datasource/alertmanager/types'; 13import { FolderDTO, NotifierDTO, ThunkResult } from 'app/types'; 14import { RuleIdentifier, RuleNamespace, RuleWithLocation, StateHistoryItem } from 'app/types/unified-alerting'; 15import { 16 PostableRulerRuleGroupDTO, 17 RulerGrafanaRuleDTO, 18 RulerRuleGroupDTO, 19 RulerRulesConfigDTO, 20} from 'app/types/unified-alerting-dto'; 21import { fetchNotifiers } from '../api/grafana'; 22import { fetchAnnotations } from '../api/annotations'; 23import { 24 expireSilence, 25 fetchAlertManagerConfig, 26 fetchAlerts, 27 fetchAlertGroups, 28 fetchSilences, 29 createOrUpdateSilence, 30 updateAlertManagerConfig, 31 fetchStatus, 32 deleteAlertManagerConfig, 33 testReceivers, 34 addAlertManagers, 35 fetchExternalAlertmanagers, 36 fetchExternalAlertmanagerConfig, 37} from '../api/alertmanager'; 38import { FetchPromRulesFilter, fetchRules } from '../api/prometheus'; 39import { 40 deleteNamespace, 41 deleteRulerRulesGroup, 42 fetchRulerRules, 43 fetchRulerRulesGroup, 44 fetchRulerRulesNamespace, 45 FetchRulerRulesFilter, 46 setRulerRuleGroup, 47} from '../api/ruler'; 48import { RuleFormType, RuleFormValues } from '../types/rule-form'; 49import { 50 getAllRulesSourceNames, 51 GRAFANA_RULES_SOURCE_NAME, 52 isGrafanaRulesSource, 53 isVanillaPrometheusAlertManagerDataSource, 54} from '../utils/datasource'; 55import { makeAMLink, retryWhile } from '../utils/misc'; 56import { withAppEvents, withSerializedError } from '../utils/redux'; 57import { formValuesToRulerRuleDTO, formValuesToRulerGrafanaRuleDTO } from '../utils/rule-form'; 58import { 59 isCloudRuleIdentifier, 60 isGrafanaRuleIdentifier, 61 isGrafanaRulerRule, 62 isPrometheusRuleIdentifier, 63 isRulerNotSupportedResponse, 64} from '../utils/rules'; 65import { addDefaultsToAlertmanagerConfig, isFetchError } from '../utils/alertmanager'; 66import * as ruleId from '../utils/rule-id'; 67import { isEmpty } from 'lodash'; 68import messageFromError from 'app/plugins/datasource/grafana-azure-monitor-datasource/utils/messageFromError'; 69import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; 70 71const FETCH_CONFIG_RETRY_TIMEOUT = 30 * 1000; 72 73export const fetchPromRulesAction = createAsyncThunk( 74 'unifiedalerting/fetchPromRules', 75 ({ rulesSourceName, filter }: { rulesSourceName: string; filter?: FetchPromRulesFilter }): Promise<RuleNamespace[]> => 76 withSerializedError(fetchRules(rulesSourceName, filter)) 77); 78 79export const fetchAlertManagerConfigAction = createAsyncThunk( 80 'unifiedalerting/fetchAmConfig', 81 (alertManagerSourceName: string): Promise<AlertManagerCortexConfig> => 82 withSerializedError( 83 (async () => { 84 // for vanilla prometheus, there is no config endpoint. Only fetch config from status 85 if (isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName)) { 86 return fetchStatus(alertManagerSourceName).then((status) => ({ 87 alertmanager_config: status.config, 88 template_files: {}, 89 })); 90 } 91 92 return retryWhile( 93 () => fetchAlertManagerConfig(alertManagerSourceName), 94 // if config has been recently deleted, it takes a while for cortex start returning the default one. 95 // retry for a short while instead of failing 96 (e) => !!messageFromError(e)?.includes('alertmanager storage object not found'), 97 FETCH_CONFIG_RETRY_TIMEOUT 98 ).then((result) => { 99 // if user config is empty for cortex alertmanager, try to get config from status endpoint 100 if ( 101 isEmpty(result.alertmanager_config) && 102 isEmpty(result.template_files) && 103 alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME 104 ) { 105 return fetchStatus(alertManagerSourceName).then((status) => ({ 106 alertmanager_config: status.config, 107 template_files: {}, 108 })); 109 } 110 return result; 111 }); 112 })() 113 ) 114); 115 116export const fetchExternalAlertmanagersAction = createAsyncThunk( 117 'unifiedAlerting/fetchExternalAlertmanagers', 118 (): Promise<ExternalAlertmanagersResponse> => { 119 return withSerializedError(fetchExternalAlertmanagers()); 120 } 121); 122 123export const fetchExternalAlertmanagersConfigAction = createAsyncThunk( 124 'unifiedAlerting/fetchExternAlertmanagersConfig', 125 (): Promise<{ alertmanagers: string[] }> => { 126 return withSerializedError(fetchExternalAlertmanagerConfig()); 127 } 128); 129 130export const fetchRulerRulesAction = createAsyncThunk( 131 'unifiedalerting/fetchRulerRules', 132 ({ 133 rulesSourceName, 134 filter, 135 }: { 136 rulesSourceName: string; 137 filter?: FetchRulerRulesFilter; 138 }): Promise<RulerRulesConfigDTO | null> => { 139 return withSerializedError(fetchRulerRules(rulesSourceName, filter)); 140 } 141); 142 143export const fetchSilencesAction = createAsyncThunk( 144 'unifiedalerting/fetchSilences', 145 (alertManagerSourceName: string): Promise<Silence[]> => { 146 return withSerializedError(fetchSilences(alertManagerSourceName)); 147 } 148); 149 150// this will only trigger ruler rules fetch if rules are not loaded yet and request is not in flight 151export function fetchRulerRulesIfNotFetchedYet(rulesSourceName: string): ThunkResult<void> { 152 return (dispatch, getStore) => { 153 const { rulerRules } = getStore().unifiedAlerting; 154 const resp = rulerRules[rulesSourceName]; 155 if (!resp?.result && !(resp && isRulerNotSupportedResponse(resp)) && !resp?.loading) { 156 dispatch(fetchRulerRulesAction({ rulesSourceName })); 157 } 158 }; 159} 160 161export function fetchAllPromAndRulerRulesAction(force = false): ThunkResult<void> { 162 return (dispatch, getStore) => { 163 const { promRules, rulerRules } = getStore().unifiedAlerting; 164 getAllRulesSourceNames().map((rulesSourceName) => { 165 if (force || !promRules[rulesSourceName]?.loading) { 166 dispatch(fetchPromRulesAction({ rulesSourceName })); 167 } 168 if (force || !rulerRules[rulesSourceName]?.loading) { 169 dispatch(fetchRulerRulesAction({ rulesSourceName })); 170 } 171 }); 172 }; 173} 174 175export function fetchAllPromRulesAction(force = false): ThunkResult<void> { 176 return (dispatch, getStore) => { 177 const { promRules } = getStore().unifiedAlerting; 178 getAllRulesSourceNames().map((rulesSourceName) => { 179 if (force || !promRules[rulesSourceName]?.loading) { 180 dispatch(fetchPromRulesAction({ rulesSourceName })); 181 } 182 }); 183 }; 184} 185 186async function findEditableRule(ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null> { 187 if (isGrafanaRuleIdentifier(ruleIdentifier)) { 188 const namespaces = await fetchRulerRules(GRAFANA_RULES_SOURCE_NAME); 189 // find namespace and group that contains the uid for the rule 190 for (const [namespace, groups] of Object.entries(namespaces)) { 191 for (const group of groups) { 192 const rule = group.rules.find( 193 (rule) => isGrafanaRulerRule(rule) && rule.grafana_alert?.uid === ruleIdentifier.uid 194 ); 195 if (rule) { 196 return { 197 group, 198 ruleSourceName: GRAFANA_RULES_SOURCE_NAME, 199 namespace: namespace, 200 rule, 201 }; 202 } 203 } 204 } 205 } 206 207 if (isCloudRuleIdentifier(ruleIdentifier)) { 208 const { ruleSourceName, namespace, groupName } = ruleIdentifier; 209 const group = await fetchRulerRulesGroup(ruleSourceName, namespace, groupName); 210 211 if (!group) { 212 return null; 213 } 214 215 const rule = group.rules.find((rule) => { 216 const identifier = ruleId.fromRulerRule(ruleSourceName, namespace, group.name, rule); 217 return ruleId.equal(identifier, ruleIdentifier); 218 }); 219 220 if (!rule) { 221 return null; 222 } 223 224 return { 225 group, 226 ruleSourceName, 227 namespace, 228 rule, 229 }; 230 } 231 232 if (isPrometheusRuleIdentifier(ruleIdentifier)) { 233 throw new Error('Native prometheus rules can not be edited in grafana.'); 234 } 235 236 return null; 237} 238 239export const fetchEditableRuleAction = createAsyncThunk( 240 'unifiedalerting/fetchEditableRule', 241 (ruleIdentifier: RuleIdentifier): Promise<RuleWithLocation | null> => 242 withSerializedError(findEditableRule(ruleIdentifier)) 243); 244 245async function deleteRule(ruleWithLocation: RuleWithLocation): Promise<void> { 246 const { ruleSourceName, namespace, group, rule } = ruleWithLocation; 247 // in case of GRAFANA, each group implicitly only has one rule. delete the group. 248 if (isGrafanaRulesSource(ruleSourceName)) { 249 await deleteRulerRulesGroup(GRAFANA_RULES_SOURCE_NAME, namespace, group.name); 250 return; 251 } 252 // in case of CLOUD 253 // it was the last rule, delete the entire group 254 if (group.rules.length === 1) { 255 await deleteRulerRulesGroup(ruleSourceName, namespace, group.name); 256 return; 257 } 258 // post the group with rule removed 259 await setRulerRuleGroup(ruleSourceName, namespace, { 260 ...group, 261 rules: group.rules.filter((r) => r !== rule), 262 }); 263} 264 265export function deleteRuleAction( 266 ruleIdentifier: RuleIdentifier, 267 options: { navigateTo?: string } = {} 268): ThunkResult<void> { 269 /* 270 * fetch the rules group from backend, delete group if it is found and+ 271 * reload ruler rules 272 */ 273 return async (dispatch) => { 274 withAppEvents( 275 (async () => { 276 const ruleWithLocation = await findEditableRule(ruleIdentifier); 277 if (!ruleWithLocation) { 278 throw new Error('Rule not found.'); 279 } 280 await deleteRule(ruleWithLocation); 281 // refetch rules for this rules source 282 dispatch(fetchRulerRulesAction({ rulesSourceName: ruleWithLocation.ruleSourceName })); 283 dispatch(fetchPromRulesAction({ rulesSourceName: ruleWithLocation.ruleSourceName })); 284 285 if (options.navigateTo) { 286 locationService.replace(options.navigateTo); 287 } 288 })(), 289 { 290 successMessage: 'Rule deleted.', 291 } 292 ); 293 }; 294} 295 296async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> { 297 const { dataSourceName, group, namespace } = values; 298 const formRule = formValuesToRulerRuleDTO(values); 299 if (dataSourceName && group && namespace) { 300 // if we're updating a rule... 301 if (existing) { 302 // refetch it so we always have the latest greatest 303 const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing)); 304 if (!freshExisting) { 305 throw new Error('Rule not found.'); 306 } 307 // if namespace or group was changed, delete the old rule 308 if (freshExisting.namespace !== namespace || freshExisting.group.name !== group) { 309 await deleteRule(freshExisting); 310 } else { 311 // if same namespace or group, update the group replacing the old rule with new 312 const payload = { 313 ...freshExisting.group, 314 rules: freshExisting.group.rules.map((existingRule) => 315 existingRule === freshExisting.rule ? formRule : existingRule 316 ), 317 }; 318 await setRulerRuleGroup(dataSourceName, namespace, payload); 319 return ruleId.fromRulerRule(dataSourceName, namespace, group, formRule); 320 } 321 } 322 323 // if creating new rule or existing rule was in a different namespace/group, create new rule in target group 324 325 const targetGroup = await fetchRulerRulesGroup(dataSourceName, namespace, group); 326 327 const payload: RulerRuleGroupDTO = targetGroup 328 ? { 329 ...targetGroup, 330 rules: [...targetGroup.rules, formRule], 331 } 332 : { 333 name: group, 334 rules: [formRule], 335 }; 336 337 await setRulerRuleGroup(dataSourceName, namespace, payload); 338 return ruleId.fromRulerRule(dataSourceName, namespace, group, formRule); 339 } else { 340 throw new Error('Data source and location must be specified'); 341 } 342} 343 344async function saveGrafanaRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> { 345 const { folder, evaluateEvery } = values; 346 const formRule = formValuesToRulerGrafanaRuleDTO(values); 347 348 if (!folder) { 349 throw new Error('Folder must be specified'); 350 } 351 352 // updating an existing rule... 353 if (existing) { 354 // refetch it to be sure we have the latest 355 const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing)); 356 if (!freshExisting) { 357 throw new Error('Rule not found.'); 358 } 359 360 // if same folder, repost the group with updated rule 361 if (freshExisting.namespace === folder.title) { 362 const uid = (freshExisting.rule as RulerGrafanaRuleDTO).grafana_alert.uid!; 363 formRule.grafana_alert.uid = uid; 364 await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, freshExisting.namespace, { 365 name: freshExisting.group.name, 366 interval: evaluateEvery, 367 rules: [formRule], 368 }); 369 return { uid }; 370 } 371 } 372 373 // if creating new rule or folder was changed, create rule in a new group 374 const targetFolderGroups = await fetchRulerRulesNamespace(GRAFANA_RULES_SOURCE_NAME, folder.title); 375 376 // set group name to rule name, but be super paranoid and check that this group does not already exist 377 const groupName = getUniqueGroupName(values.name, targetFolderGroups); 378 formRule.grafana_alert.title = groupName; 379 380 const payload: PostableRulerRuleGroupDTO = { 381 name: groupName, 382 interval: evaluateEvery, 383 rules: [formRule], 384 }; 385 await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, payload); 386 387 // now refetch this group to get the uid, hah 388 const result = await fetchRulerRulesGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, groupName); 389 const newUid = (result?.rules[0] as RulerGrafanaRuleDTO)?.grafana_alert?.uid; 390 if (newUid) { 391 // if folder has changed, delete the old one 392 if (existing) { 393 const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing)); 394 if (freshExisting && freshExisting.namespace !== folder.title) { 395 await deleteRule(freshExisting); 396 } 397 } 398 399 return { uid: newUid }; 400 } else { 401 throw new Error('Failed to fetch created rule.'); 402 } 403} 404 405export function getUniqueGroupName(currentGroupName: string, existingGroups: RulerRuleGroupDTO[]) { 406 let newGroupName = currentGroupName; 407 let idx = 1; 408 while (!!existingGroups.find((g) => g.name === newGroupName)) { 409 newGroupName = `${currentGroupName}-${++idx}`; 410 } 411 412 return newGroupName; 413} 414 415export const saveRuleFormAction = createAsyncThunk( 416 'unifiedalerting/saveRuleForm', 417 ({ 418 values, 419 existing, 420 redirectOnSave, 421 }: { 422 values: RuleFormValues; 423 existing?: RuleWithLocation; 424 redirectOnSave?: string; 425 }): Promise<void> => 426 withAppEvents( 427 withSerializedError( 428 (async () => { 429 const { type } = values; 430 // in case of system (cortex/loki) 431 let identifier: RuleIdentifier; 432 if (type === RuleFormType.cloudAlerting || type === RuleFormType.cloudRecording) { 433 identifier = await saveLotexRule(values, existing); 434 // in case of grafana managed 435 } else if (type === RuleFormType.grafana) { 436 identifier = await saveGrafanaRule(values, existing); 437 } else { 438 throw new Error('Unexpected rule form type'); 439 } 440 if (redirectOnSave) { 441 locationService.push(redirectOnSave); 442 } else { 443 // redirect to edit page 444 const newLocation = `/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`; 445 if (locationService.getLocation().pathname !== newLocation) { 446 locationService.replace(newLocation); 447 } 448 } 449 })() 450 ), 451 { 452 successMessage: existing ? `Rule "${values.name}" updated.` : `Rule "${values.name}" saved.`, 453 errorMessage: 'Failed to save rule', 454 } 455 ) 456); 457 458export const fetchGrafanaNotifiersAction = createAsyncThunk( 459 'unifiedalerting/fetchGrafanaNotifiers', 460 (): Promise<NotifierDTO[]> => withSerializedError(fetchNotifiers()) 461); 462 463export const fetchGrafanaAnnotationsAction = createAsyncThunk( 464 'unifiedalerting/fetchGrafanaAnnotations', 465 (alertId: string): Promise<StateHistoryItem[]> => withSerializedError(fetchAnnotations(alertId)) 466); 467 468interface UpdateAlertManagerConfigActionOptions { 469 alertManagerSourceName: string; 470 oldConfig: AlertManagerCortexConfig; // it will be checked to make sure it didn't change in the meanwhile 471 newConfig: AlertManagerCortexConfig; 472 successMessage?: string; // show toast on success 473 redirectPath?: string; // where to redirect on success 474 refetch?: boolean; // refetch config on success 475} 476 477export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlertManagerConfigActionOptions, {}>( 478 'unifiedalerting/updateAMConfig', 479 ({ alertManagerSourceName, oldConfig, newConfig, successMessage, redirectPath, refetch }, thunkAPI): Promise<void> => 480 withAppEvents( 481 withSerializedError( 482 (async () => { 483 const latestConfig = await fetchAlertManagerConfig(alertManagerSourceName); 484 if ( 485 !(isEmpty(latestConfig.alertmanager_config) && isEmpty(latestConfig.template_files)) && 486 JSON.stringify(latestConfig) !== JSON.stringify(oldConfig) 487 ) { 488 throw new Error( 489 'It seems configuration has been recently updated. Please reload page and try again to make sure that recent changes are not overwritten.' 490 ); 491 } 492 await updateAlertManagerConfig(alertManagerSourceName, addDefaultsToAlertmanagerConfig(newConfig)); 493 if (refetch) { 494 await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); 495 } 496 if (redirectPath) { 497 locationService.push(makeAMLink(redirectPath, alertManagerSourceName)); 498 } 499 })() 500 ), 501 { 502 successMessage, 503 } 504 ) 505); 506 507export const fetchAmAlertsAction = createAsyncThunk( 508 'unifiedalerting/fetchAmAlerts', 509 (alertManagerSourceName: string): Promise<AlertmanagerAlert[]> => 510 withSerializedError(fetchAlerts(alertManagerSourceName, [], true, true, true)) 511); 512 513export const expireSilenceAction = (alertManagerSourceName: string, silenceId: string): ThunkResult<void> => { 514 return async (dispatch) => { 515 await withAppEvents(expireSilence(alertManagerSourceName, silenceId), { 516 successMessage: 'Silence expired.', 517 }); 518 dispatch(fetchSilencesAction(alertManagerSourceName)); 519 dispatch(fetchAmAlertsAction(alertManagerSourceName)); 520 }; 521}; 522 523type UpdateSilenceActionOptions = { 524 alertManagerSourceName: string; 525 payload: SilenceCreatePayload; 526 exitOnSave: boolean; 527 successMessage?: string; 528}; 529 530export const createOrUpdateSilenceAction = createAsyncThunk<void, UpdateSilenceActionOptions, {}>( 531 'unifiedalerting/updateSilence', 532 ({ alertManagerSourceName, payload, exitOnSave, successMessage }): Promise<void> => 533 withAppEvents( 534 withSerializedError( 535 (async () => { 536 await createOrUpdateSilence(alertManagerSourceName, payload); 537 if (exitOnSave) { 538 locationService.push('/alerting/silences'); 539 } 540 })() 541 ), 542 { 543 successMessage, 544 } 545 ) 546); 547 548export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult<void> => { 549 return (dispatch, getState) => { 550 const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result; 551 if (!config) { 552 throw new Error(`Config for ${alertManagerSourceName} not found`); 553 } 554 if (!config.alertmanager_config.receivers?.find((receiver) => receiver.name === receiverName)) { 555 throw new Error(`Cannot delete receiver ${receiverName}: not found in config.`); 556 } 557 const newConfig: AlertManagerCortexConfig = { 558 ...config, 559 alertmanager_config: { 560 ...config.alertmanager_config, 561 receivers: config.alertmanager_config.receivers.filter((receiver) => receiver.name !== receiverName), 562 }, 563 }; 564 return dispatch( 565 updateAlertManagerConfigAction({ 566 newConfig, 567 oldConfig: config, 568 alertManagerSourceName, 569 successMessage: 'Contact point deleted.', 570 refetch: true, 571 }) 572 ); 573 }; 574}; 575 576export const deleteTemplateAction = (templateName: string, alertManagerSourceName: string): ThunkResult<void> => { 577 return (dispatch, getState) => { 578 const config = getState().unifiedAlerting.amConfigs?.[alertManagerSourceName]?.result; 579 if (!config) { 580 throw new Error(`Config for ${alertManagerSourceName} not found`); 581 } 582 if (typeof config.template_files?.[templateName] !== 'string') { 583 throw new Error(`Cannot delete template ${templateName}: not found in config.`); 584 } 585 const newTemplates = { ...config.template_files }; 586 delete newTemplates[templateName]; 587 const newConfig: AlertManagerCortexConfig = { 588 ...config, 589 alertmanager_config: { 590 ...config.alertmanager_config, 591 templates: config.alertmanager_config.templates?.filter((existing) => existing !== templateName), 592 }, 593 template_files: newTemplates, 594 }; 595 return dispatch( 596 updateAlertManagerConfigAction({ 597 newConfig, 598 oldConfig: config, 599 alertManagerSourceName, 600 successMessage: 'Template deleted.', 601 refetch: true, 602 }) 603 ); 604 }; 605}; 606 607export const fetchFolderAction = createAsyncThunk( 608 'unifiedalerting/fetchFolder', 609 (uid: string): Promise<FolderDTO> => withSerializedError((getBackendSrv() as any).getFolderByUid(uid)) 610); 611 612export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult<void> => { 613 return (dispatch, getState) => { 614 if (!getState().unifiedAlerting.folders[uid]?.dispatched) { 615 dispatch(fetchFolderAction(uid)); 616 } 617 }; 618}; 619 620export const fetchAlertGroupsAction = createAsyncThunk( 621 'unifiedalerting/fetchAlertGroups', 622 (alertManagerSourceName: string): Promise<AlertmanagerGroup[]> => { 623 return withSerializedError(fetchAlertGroups(alertManagerSourceName)); 624 } 625); 626 627export const checkIfLotexSupportsEditingRulesAction = createAsyncThunk<boolean, string>( 628 'unifiedalerting/checkIfLotexRuleEditingSupported', 629 async (rulesSourceName: string): Promise<boolean> => 630 withAppEvents( 631 (async () => { 632 try { 633 await fetchRulerRulesGroup(rulesSourceName, 'test', 'test'); 634 return true; 635 } catch (e) { 636 if ( 637 (isFetchError(e) && 638 (e.data.message?.includes('GetRuleGroup unsupported in rule local store') || // "local" rule storage 639 e.data.message?.includes('page not found'))) || // ruler api disabled 640 e.message?.includes('404 from rules config endpoint') || // ruler api disabled 641 e.data.message?.includes(RULER_NOT_SUPPORTED_MSG) // ruler api not supported 642 ) { 643 return false; 644 } 645 throw e; 646 } 647 })(), 648 { 649 errorMessage: `Failed to determine if "${rulesSourceName}" allows editing rules`, 650 } 651 ) 652); 653 654export const deleteAlertManagerConfigAction = createAsyncThunk( 655 'unifiedalerting/deleteAlertManagerConfig', 656 async (alertManagerSourceName: string, thunkAPI): Promise<void> => { 657 return withAppEvents( 658 withSerializedError( 659 (async () => { 660 await deleteAlertManagerConfig(alertManagerSourceName); 661 await thunkAPI.dispatch(fetchAlertManagerConfigAction(alertManagerSourceName)); 662 })() 663 ), 664 { 665 errorMessage: 'Failed to reset Alertmanager configuration', 666 successMessage: 'Alertmanager configuration reset.', 667 } 668 ); 669 } 670); 671 672interface TestReceiversOptions { 673 alertManagerSourceName: string; 674 receivers: Receiver[]; 675 alert?: TestReceiversAlert; 676} 677 678export const testReceiversAction = createAsyncThunk( 679 'unifiedalerting/testReceivers', 680 ({ alertManagerSourceName, receivers, alert }: TestReceiversOptions): Promise<void> => { 681 return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers, alert)), { 682 errorMessage: 'Failed to send test alert.', 683 successMessage: 'Test alert sent.', 684 }); 685 } 686); 687 688interface UpdateNamespaceAndGroupOptions { 689 rulesSourceName: string; 690 namespaceName: string; 691 groupName: string; 692 newNamespaceName: string; 693 newGroupName: string; 694 groupInterval?: string; 695} 696 697// allows renaming namespace, renaming group and changing group interval, all in one go 698export const updateLotexNamespaceAndGroupAction = createAsyncThunk( 699 'unifiedalerting/updateLotexNamespaceAndGroup', 700 async (options: UpdateNamespaceAndGroupOptions, thunkAPI): Promise<void> => { 701 return withAppEvents( 702 withSerializedError( 703 (async () => { 704 const { rulesSourceName, namespaceName, groupName, newNamespaceName, newGroupName, groupInterval } = options; 705 if (options.rulesSourceName === GRAFANA_RULES_SOURCE_NAME) { 706 throw new Error(`this action does not support Grafana rules`); 707 } 708 // fetch rules and perform sanity checks 709 const rulesResult = await fetchRulerRules(rulesSourceName); 710 if (!rulesResult[namespaceName]) { 711 throw new Error(`Namespace "${namespaceName}" not found.`); 712 } 713 const existingGroup = rulesResult[namespaceName].find((group) => group.name === groupName); 714 if (!existingGroup) { 715 throw new Error(`Group "${groupName}" not found.`); 716 } 717 if (newGroupName !== groupName && !!rulesResult[namespaceName].find((group) => group.name === newGroupName)) { 718 throw new Error(`Group "${newGroupName}" already exists.`); 719 } 720 if (newNamespaceName !== namespaceName && !!rulesResult[newNamespaceName]) { 721 throw new Error(`Namespace "${newNamespaceName}" already exists.`); 722 } 723 if ( 724 newNamespaceName === namespaceName && 725 groupName === newGroupName && 726 groupInterval === existingGroup.interval 727 ) { 728 throw new Error('Nothing changed.'); 729 } 730 731 // if renaming namespace - make new copies of all groups, then delete old namespace 732 if (newNamespaceName !== namespaceName) { 733 for (const group of rulesResult[namespaceName]) { 734 await setRulerRuleGroup( 735 rulesSourceName, 736 newNamespaceName, 737 group.name === groupName 738 ? { 739 ...group, 740 name: newGroupName, 741 interval: groupInterval, 742 } 743 : group 744 ); 745 } 746 await deleteNamespace(rulesSourceName, namespaceName); 747 748 // if only modifying group... 749 } else { 750 // save updated group 751 await setRulerRuleGroup(rulesSourceName, namespaceName, { 752 ...existingGroup, 753 name: newGroupName, 754 interval: groupInterval, 755 }); 756 // if group name was changed, delete old group 757 if (newGroupName !== groupName) { 758 await deleteRulerRulesGroup(rulesSourceName, namespaceName, groupName); 759 } 760 } 761 762 // refetch all rules 763 await thunkAPI.dispatch(fetchRulerRulesAction({ rulesSourceName })); 764 })() 765 ), 766 { 767 errorMessage: 'Failed to update namespace / group', 768 successMessage: 'Update successful', 769 } 770 ); 771 } 772); 773 774export const addExternalAlertmanagersAction = createAsyncThunk( 775 'unifiedAlerting/addExternalAlertmanagers', 776 async (alertManagerUrls: string[], thunkAPI): Promise<void> => { 777 return withAppEvents( 778 withSerializedError( 779 (async () => { 780 await addAlertManagers(alertManagerUrls); 781 thunkAPI.dispatch(fetchExternalAlertmanagersConfigAction()); 782 })() 783 ), 784 { 785 errorMessage: 'Failed adding alertmanagers', 786 successMessage: 'Alertmanagers updated', 787 } 788 ); 789 } 790); 791