Alerting: Remove __grafana_origin when duplicating rule (#112396)

* remove __grafana_origin when duplicating rule

* Fix origin removal condition to handle cloud rules

* Improve origin label condition, improve tests

* Fix import path

---------

Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
Sonia Aguilar
2025-10-15 14:18:50 +02:00
committed by GitHub
co-authored by Konrad Lalik
parent b7a6e1ca53
commit 6baa257f91
4 changed files with 270 additions and 138 deletions
@@ -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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerAlertingRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerRecordingRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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('');
});
});
});
@@ -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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerAlertingRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerRecordingRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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<RulerAlertingRuleDTO> = {
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<RulerGrafanaRuleDTO> = {
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<RulerRecordingRuleDTO> = {
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<RulerGrafanaRuleDTO> = {
ruleSourceName: 'my-prom-ds',
namespace: 'namespace-one',
group: mockRulerRuleGroup(),
rule,
};
const clonedRule: RuleWithLocation<RulerRuleDTO> = 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');
});
});
@@ -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<RulerRuleDTO>) {
}
}
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;
}
@@ -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),
},