diff --git a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx index 8b31e3a946a..2572d9ddfc5 100644 --- a/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/rule-editor/CloneRuleEditor.test.tsx @@ -6,24 +6,10 @@ import { byRole, byTestId } from 'testing-library-selector'; import { MIMIR_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants'; import { DashboardSearchItemType } from 'app/features/search/types'; import { AccessControlAction } from 'app/types/accessControl'; -import { RuleWithLocation } from 'app/types/unified-alerting'; -import { - RulerAlertingRuleDTO, - RulerGrafanaRuleDTO, - RulerRecordingRuleDTO, - RulerRuleDTO, -} from 'app/types/unified-alerting-dto'; import { ExpressionEditorProps } from '../components/rule-editor/ExpressionEditor'; import { setupMswServer } from '../mockApi'; -import { - grantUserPermissions, - mockDataSource, - mockFolder, - mockRulerAlertingRule, - mockRulerGrafanaRule, - mockRulerRuleGroup, -} from '../mocks'; +import { grantUserPermissions, mockDataSource, mockFolder, mockRulerAlertingRule } from '../mocks'; import { grafanaRulerRule, mockPreviewApiResponse } from '../mocks/grafanaRulerApi'; import { mockRulerRulesApiResponse, mockRulerRulesGroupApiResponse } from '../mocks/rulerApi'; import { setFolderResponse } from '../mocks/server/configure'; @@ -34,7 +20,6 @@ import { Annotation } from '../utils/constants'; import { hashRulerRule } from '../utils/rule-id'; import { ExistingRuleEditor } from './ExistingRuleEditor'; -import { cloneRuleDefinition } from './clone.utils'; import { getDefaultFormValues } from './formDefaults'; jest.mock('../components/rule-editor/ExpressionEditor', () => ({ @@ -198,125 +183,4 @@ describe('CloneRuleEditor', function () { expect(ui.inputs.annotationValue(0).get()).toHaveTextContent('This is a very important alert rule'); }); }); - - describe('cloneRuleDefinition', () => { - it("Should change the cloned rule's name accordingly for Grafana rules", () => { - const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( - { - for: '1m', - labels: { severity: 'critical', region: 'nasa' }, - annotations: { [Annotation.summary]: 'This is a very important alert rule' }, - }, - { uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] } - ); - - const originalRule: RuleWithLocation = { - ruleSourceName: 'my-prom-ds', - namespace: 'namespace-one', - group: mockRulerRuleGroup(), - rule, - }; - - const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); - - const grafanaRule: RulerGrafanaRuleDTO = clonedRule.rule as RulerGrafanaRuleDTO; - - expect(originalRule.rule.grafana_alert.title).toEqual('First Grafana Rule'); - expect(grafanaRule.grafana_alert.title).toEqual('First Grafana Rule (copy)'); - }); - - it("Should change the cloned rule's name accordingly for Ruler rules", () => { - const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({ - for: '1m', - alert: 'First Ruler Rule', - expr: 'vector(1) > 0', - labels: { severity: 'critical', region: 'nasa' }, - annotations: { [Annotation.summary]: 'This is a very important alert rule' }, - }); - - const originalRule: RuleWithLocation = { - ruleSourceName: 'my-prom-ds', - namespace: 'namespace-one', - group: mockRulerRuleGroup(), - rule, - }; - - const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); - - const alertingRule: RulerAlertingRuleDTO = clonedRule.rule as RulerAlertingRuleDTO; - - expect(originalRule.rule.alert).toEqual('First Ruler Rule'); - expect(alertingRule.alert).toEqual('First Ruler Rule (copy)'); - }); - - it("Should change the cloned rule's name accordingly for Recording rules", () => { - const rule: RulerRecordingRuleDTO = { - record: 'instance:node_num_cpu:sum', - expr: 'count without (cpu) (count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"}))', - labels: { type: 'cpu' }, - }; - - const originalRule: RuleWithLocation = { - ruleSourceName: 'my-prom-ds', - namespace: 'namespace-one', - group: mockRulerRuleGroup(), - rule, - }; - - const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); - - const recordingRule: RulerRecordingRuleDTO = clonedRule.rule as RulerRecordingRuleDTO; - - expect(originalRule.rule.record).toEqual('instance:node_num_cpu:sum'); - expect(recordingRule.record).toEqual('instance:node_num_cpu:sum (copy)'); - }); - - it('Should remove the group for provisioned Grafana rules', () => { - const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( - { - for: '1m', - labels: { severity: 'critical', region: 'nasa' }, - annotations: { [Annotation.summary]: 'This is a very important alert rule' }, - }, - { uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [], provenance: 'foo' } - ); - - const originalRule: RuleWithLocation = { - ruleSourceName: 'my-prom-ds', - namespace: 'namespace-one', - group: mockRulerRuleGroup(), - rule, - }; - - const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); - - expect(originalRule.group.name).toEqual('group1'); - expect(clonedRule.group.name).toEqual(''); - }); - - it('The cloned rule should not contain a UID property', () => { - const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( - { - for: '1m', - labels: { severity: 'critical', region: 'nasa' }, - annotations: { [Annotation.summary]: 'This is a very important alert rule' }, - }, - { uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] } - ); - - const originalRule: RuleWithLocation = { - ruleSourceName: 'my-prom-ds', - namespace: 'namespace-one', - group: mockRulerRuleGroup(), - rule, - }; - - const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); - - const grafanaRule: RulerGrafanaRuleDTO = clonedRule.rule as RulerGrafanaRuleDTO; - - expect(originalRule.rule.grafana_alert.uid).toEqual('grafana-rule-1'); - expect(grafanaRule.grafana_alert.uid).toEqual(''); - }); - }); }); diff --git a/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx b/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx new file mode 100644 index 00000000000..ff2f8f945db --- /dev/null +++ b/public/app/features/alerting/unified/rule-editor/clone.utils.test.tsx @@ -0,0 +1,261 @@ +import { config } from '@grafana/runtime'; +import { RuleWithLocation } from 'app/types/unified-alerting'; +import { + RulerAlertingRuleDTO, + RulerGrafanaRuleDTO, + RulerRecordingRuleDTO, + RulerRuleDTO, +} from 'app/types/unified-alerting-dto'; + +import { mockRulerAlertingRule, mockRulerGrafanaRule, mockRulerRuleGroup } from '../mocks'; +import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; +import { SupportedPlugin } from '../types/pluginBridges'; +import { Annotation } from '../utils/constants'; +import { GRAFANA_ORIGIN_LABEL } from '../utils/labels'; + +import { cloneRuleDefinition } from './clone.utils'; + +describe('cloneRuleDefinition', () => { + it("Should change the cloned rule's name accordingly for Grafana rules", () => { + const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( + { + for: '1m', + labels: { severity: 'critical', region: 'nasa' }, + annotations: { [Annotation.summary]: 'This is a very important alert rule' }, + }, + { uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] } + ); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); + + const grafanaRule: RulerGrafanaRuleDTO = clonedRule.rule as RulerGrafanaRuleDTO; + + expect(originalRule.rule.grafana_alert.title).toEqual('First Grafana Rule'); + expect(grafanaRule.grafana_alert.title).toEqual('First Grafana Rule (copy)'); + }); + + it("Should change the cloned rule's name accordingly for Ruler rules", () => { + const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({ + for: '1m', + alert: 'First Ruler Rule', + expr: 'vector(1) > 0', + labels: { severity: 'critical', region: 'nasa' }, + annotations: { [Annotation.summary]: 'This is a very important alert rule' }, + }); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); + + const alertingRule: RulerAlertingRuleDTO = clonedRule.rule as RulerAlertingRuleDTO; + + expect(originalRule.rule.alert).toEqual('First Ruler Rule'); + expect(alertingRule.alert).toEqual('First Ruler Rule (copy)'); + }); + + it("Should change the cloned rule's name accordingly for Recording rules", () => { + const rule: RulerRecordingRuleDTO = { + record: 'instance:node_num_cpu:sum', + expr: 'count without (cpu) (count without (mode) (node_cpu_seconds_total{job="integrations/node_exporter"}))', + labels: { type: 'cpu' }, + }; + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); + + const recordingRule: RulerRecordingRuleDTO = clonedRule.rule as RulerRecordingRuleDTO; + + expect(originalRule.rule.record).toEqual('instance:node_num_cpu:sum'); + expect(recordingRule.record).toEqual('instance:node_num_cpu:sum (copy)'); + }); + + it('Should remove the group for provisioned Grafana rules', () => { + const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( + { + for: '1m', + labels: { severity: 'critical', region: 'nasa' }, + annotations: { [Annotation.summary]: 'This is a very important alert rule' }, + }, + { uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [], provenance: 'foo' } + ); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); + + expect(originalRule.group.name).toEqual('group1'); + expect(clonedRule.group.name).toEqual(''); + }); + + it('The cloned rule should not contain a UID property', () => { + const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( + { + for: '1m', + labels: { severity: 'critical', region: 'nasa' }, + annotations: { [Annotation.summary]: 'This is a very important alert rule' }, + }, + { uid: 'grafana-rule-1', title: 'First Grafana Rule', data: [] } + ); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); + + const grafanaRule: RulerGrafanaRuleDTO = clonedRule.rule as RulerGrafanaRuleDTO; + + expect(originalRule.rule.grafana_alert.uid).toEqual('grafana-rule-1'); + expect(grafanaRule.grafana_alert.uid).toEqual(''); + }); + + it('Should remove the origin label when cloning data source plugin-provided rules', () => { + // Mock the plugin as installed + config.apps = { + [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]), + }; + + const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({ + alert: 'slo-provider-alert', + expr: 'vector(1) > 0', + for: '1m', + labels: { + severity: 'critical', + region: 'nasa', + [GRAFANA_ORIGIN_LABEL]: 'plugin/' + SupportedPlugin.Slo, + }, + annotations: { [Annotation.summary]: 'This is a plugin-provided alert rule' }, + }); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const { rule: clonedRule } = cloneRuleDefinition(originalRule); + + // Original rule should have the origin label + expect(originalRule.rule.labels?.[GRAFANA_ORIGIN_LABEL]).toEqual('plugin/' + SupportedPlugin.Slo); + + // Cloned rule should not have the origin label + expect(clonedRule.labels?.[GRAFANA_ORIGIN_LABEL]).toBeUndefined(); + + // Other labels should be preserved + expect(clonedRule.labels?.severity).toEqual('critical'); + expect(clonedRule.labels?.region).toEqual('nasa'); + }); + + it('Should remove the origin label when cloning Grafana-managed plugin-provided rules', () => { + config.apps = { + [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]), + }; + + const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( + { + for: '1m', + labels: { + severity: 'critical', + [GRAFANA_ORIGIN_LABEL]: 'plugin/' + SupportedPlugin.Slo, + }, + annotations: { [Annotation.summary]: 'Plugin-provided Grafana rule' }, + }, + { uid: 'grafana-plugin-rule', title: 'Plugin Grafana Rule', data: [] } + ); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'grafana', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const { rule: clonedRule } = cloneRuleDefinition(originalRule); + + expect(originalRule.rule.labels?.[GRAFANA_ORIGIN_LABEL]).toEqual('plugin/' + SupportedPlugin.Slo); + expect(clonedRule.labels?.[GRAFANA_ORIGIN_LABEL]).toBeUndefined(); + expect(clonedRule.labels?.severity).toEqual('critical'); + }); + + it('Should remove the origin label when cloning data source recording rules', () => { + const rule: RulerRecordingRuleDTO = { + record: 'plugin:recording:rule', + expr: 'sum(metric)', + labels: { + type: 'cpu', + [GRAFANA_ORIGIN_LABEL]: 'plugin/' + SupportedPlugin.Slo, + }, + }; + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const { rule: clonedRule } = cloneRuleDefinition(originalRule); + + expect(originalRule.rule.labels?.[GRAFANA_ORIGIN_LABEL]).toBeDefined(); + expect(clonedRule.labels?.[GRAFANA_ORIGIN_LABEL]).toBeUndefined(); + expect(clonedRule.labels?.type).toEqual('cpu'); + }); + + it('Should preserve all labels when cloning non-plugin-provided rules', () => { + const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule( + { + for: '1m', + labels: { + severity: 'critical', + region: 'nasa', + custom_label: 'custom_value', + }, + annotations: { [Annotation.summary]: 'This is a regular alert rule' }, + }, + { uid: 'regular-rule-1', title: 'Regular Alert Rule', data: [] } + ); + + const originalRule: RuleWithLocation = { + ruleSourceName: 'my-prom-ds', + namespace: 'namespace-one', + group: mockRulerRuleGroup(), + rule, + }; + + const clonedRule: RuleWithLocation = cloneRuleDefinition(originalRule); + + const grafanaRule: RulerGrafanaRuleDTO = clonedRule.rule as RulerGrafanaRuleDTO; + + // All labels should be preserved for non-plugin rules + expect(grafanaRule.labels?.severity).toEqual('critical'); + expect(grafanaRule.labels?.region).toEqual('nasa'); + expect(grafanaRule.labels?.custom_label).toEqual('custom_value'); + }); +}); diff --git a/public/app/features/alerting/unified/rule-editor/clone.utils.ts b/public/app/features/alerting/unified/rule-editor/clone.utils.ts index a4b38a3b7a7..f109866ef61 100644 --- a/public/app/features/alerting/unified/rule-editor/clone.utils.ts +++ b/public/app/features/alerting/unified/rule-editor/clone.utils.ts @@ -4,7 +4,8 @@ import { RuleWithLocation } from 'app/types/unified-alerting'; import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { generateCopiedName } from '../utils/duplicate'; -import { getRuleName, rulerRuleType } from '../utils/rules'; +import { GRAFANA_ORIGIN_LABEL } from '../utils/labels'; +import { getRuleName, isPluginProvidedRule, rulerRuleType } from '../utils/rules'; export function changeRuleName(rule: RulerRuleDTO, newName: string) { if (rulerRuleType.grafana.rule(rule)) { @@ -35,5 +36,10 @@ export function cloneRuleDefinition(rule: RuleWithLocation) { } } + if (rulerRuleType.any.rule(ruleClone.rule) && isPluginProvidedRule(ruleClone.rule)) { + // Remove the origin label when cloning plugin-provided rules + delete ruleClone.rule.labels?.[GRAFANA_ORIGIN_LABEL]; + } + return ruleClone; } diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index 0778bcc2903..e3d35972be5 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -120,6 +120,7 @@ export const rulerRuleType = { recordingRule: isCloudRecordingRulerRule, }, any: { + rule: (rule?: RulerRuleDTO) => isCloudRulerRule(rule) || isGrafanaRulerRule(rule), recordingRule: (rule?: RulerRuleDTO) => isCloudRecordingRulerRule(rule) || isGrafanaRecordingRule(rule), alertingRule: (rule?: RulerRuleDTO) => isAlertingRulerRule(rule) || isGrafanaAlertingRule(rule), },