From 88f3dff6d2554a945c636723f028073b404ac718 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Mon, 31 Jan 2022 12:09:06 +0100 Subject: [PATCH] Alerting: Fix moving alert rules to a different folder (#44533) * Prevent rule deletion when creation in a new folder fails * Fix handling duplication of rules when moving between folders * Add unit test for unique name generator * Fix typo --- .../alerting/unified/state/actions.test.ts | 31 +++++ .../alerting/unified/state/actions.ts | 114 ++++++++++-------- 2 files changed, 94 insertions(+), 51 deletions(-) create mode 100644 public/app/features/alerting/unified/state/actions.test.ts diff --git a/public/app/features/alerting/unified/state/actions.test.ts b/public/app/features/alerting/unified/state/actions.test.ts new file mode 100644 index 00000000000..99ac8dc0e06 --- /dev/null +++ b/public/app/features/alerting/unified/state/actions.test.ts @@ -0,0 +1,31 @@ +import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto'; +import { getUniqueGroupName } from './actions'; + +describe('getUniqueGroupName', () => { + it('Should return the original value when there are no duplicates', () => { + // Arrange + const originalGroupName = 'file-system-out-of-space'; + const existingGroups: RulerRuleGroupDTO[] = []; + + // Act + const groupName = getUniqueGroupName(originalGroupName, existingGroups); + + // Assert + expect(groupName).toBe(originalGroupName); + }); + + it('Should increment suffix counter until a unique name created', () => { + // Arrange + const originalGroupName = 'file-system-out-of-space'; + const existingGroups: RulerRuleGroupDTO[] = [ + { name: 'file-system-out-of-space', rules: [] }, + { name: 'file-system-out-of-space-2', rules: [] }, + ]; + + // Act + const groupName = getUniqueGroupName(originalGroupName, existingGroups); + + // Assert + expect(groupName).toBe('file-system-out-of-space-3'); + }); +}); diff --git a/public/app/features/alerting/unified/state/actions.ts b/public/app/features/alerting/unified/state/actions.ts index ab87c0557ff..048717794bf 100644 --- a/public/app/features/alerting/unified/state/actions.ts +++ b/public/app/features/alerting/unified/state/actions.ts @@ -343,60 +343,72 @@ async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation async function saveGrafanaRule(values: RuleFormValues, existing?: RuleWithLocation): Promise { const { folder, evaluateEvery } = values; const formRule = formValuesToRulerGrafanaRuleDTO(values); - if (folder) { - // updating an existing rule... - if (existing) { - // refetch it to be sure we have the latest - const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing)); - if (!freshExisting) { - throw new Error('Rule not found.'); - } - // if folder has changed, delete the old one - if (freshExisting.namespace !== folder.title) { - await deleteRule(freshExisting); - // if same folder, repost the group with updated rule - } else { - const uid = (freshExisting.rule as RulerGrafanaRuleDTO).grafana_alert.uid!; - formRule.grafana_alert.uid = uid; - await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, freshExisting.namespace, { - name: freshExisting.group.name, - interval: evaluateEvery, - rules: [formRule], - }); - return { uid }; - } - } - - // if creating new rule or folder was changed, create rule in a new group - - const existingNamespace = await fetchRulerRulesNamespace(GRAFANA_RULES_SOURCE_NAME, folder.title); - - // set group name to rule name, but be super paranoid and check that this group does not already exist - let group = values.name; - let idx = 1; - while (!!existingNamespace.find((g) => g.name === group)) { - group = `${values.name}-${++idx}`; - } - - const payload: PostableRulerRuleGroupDTO = { - name: group, - interval: evaluateEvery, - rules: [formRule], - }; - await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, payload); - - // now refetch this group to get the uid, hah - const result = await fetchRulerRulesGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, group); - const newUid = (result?.rules[0] as RulerGrafanaRuleDTO)?.grafana_alert?.uid; - if (newUid) { - return { uid: newUid }; - } else { - throw new Error('Failed to fetch created rule.'); - } - } else { + if (!folder) { throw new Error('Folder must be specified'); } + + // updating an existing rule... + if (existing) { + // refetch it to be sure we have the latest + const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing)); + if (!freshExisting) { + throw new Error('Rule not found.'); + } + + // if same folder, repost the group with updated rule + if (freshExisting.namespace === folder.title) { + const uid = (freshExisting.rule as RulerGrafanaRuleDTO).grafana_alert.uid!; + formRule.grafana_alert.uid = uid; + await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, freshExisting.namespace, { + name: freshExisting.group.name, + interval: evaluateEvery, + rules: [formRule], + }); + return { uid }; + } + } + + // if creating new rule or folder was changed, create rule in a new group + const targetFolderGroups = await fetchRulerRulesNamespace(GRAFANA_RULES_SOURCE_NAME, folder.title); + + // set group name to rule name, but be super paranoid and check that this group does not already exist + const groupName = getUniqueGroupName(values.name, targetFolderGroups); + formRule.grafana_alert.title = groupName; + + const payload: PostableRulerRuleGroupDTO = { + name: groupName, + interval: evaluateEvery, + rules: [formRule], + }; + await setRulerRuleGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, payload); + + // now refetch this group to get the uid, hah + const result = await fetchRulerRulesGroup(GRAFANA_RULES_SOURCE_NAME, folder.title, groupName); + const newUid = (result?.rules[0] as RulerGrafanaRuleDTO)?.grafana_alert?.uid; + if (newUid) { + // if folder has changed, delete the old one + if (existing) { + const freshExisting = await findEditableRule(ruleId.fromRuleWithLocation(existing)); + if (freshExisting && freshExisting.namespace !== folder.title) { + await deleteRule(freshExisting); + } + } + + return { uid: newUid }; + } else { + throw new Error('Failed to fetch created rule.'); + } +} + +export function getUniqueGroupName(currentGroupName: string, existingGroups: RulerRuleGroupDTO[]) { + let newGroupName = currentGroupName; + let idx = 1; + while (!!existingGroups.find((g) => g.name === newGroupName)) { + newGroupName = `${currentGroupName}-${++idx}`; + } + + return newGroupName; } export const saveRuleFormAction = createAsyncThunk(