Alerting: Do not auto-populate target datasource if it's not selectable (#106912)

This commit is contained in:
Alexander Akhmetov
2025-06-24 16:41:26 +02:00
committed by GitHub
parent 2afcb0e95b
commit 8d9dec6979
2 changed files with 103 additions and 7 deletions
@@ -1,5 +1,5 @@
import { GrafanaConfig } from '@grafana/data';
import { config } from '@grafana/runtime';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { mockAlertQuery, mockDataSource, mockReduceExpression, mockThresholdExpression } from '../mocks';
import { testWithFeatureToggles } from '../test/test-utils';
@@ -8,6 +8,11 @@ import { Annotation } from '../utils/constants';
import { DataSourceType, getDefaultOrFirstCompatibleDataSource } from '../utils/datasource';
import { MANUAL_ROUTING_KEY, getDefaultQueries } from '../utils/rule-form';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: jest.fn(),
}));
import { formValuesFromQueryParams, getDefaultFormValues, getDefautManualRouting } from './formDefaults';
import { isAlertQueryOfAlertData } from './formProcessing';
@@ -200,21 +205,91 @@ describe('getDefaultFormValues', () => {
const grafanaConfig: GrafanaConfig = config;
const uaConfig = grafanaConfig.unifiedAlerting;
afterEach(() => {
uaConfig.defaultRecordingRulesTargetDatasourceUID = undefined;
const mockGetInstanceSettings = jest.fn();
beforeEach(() => {
(getDataSourceSrv as jest.Mock).mockReturnValue({
getInstanceSettings: mockGetInstanceSettings,
});
});
it('should set targetDatasourceUid from config when defaultRecordingRulesTargetDatasourceUID is provided', () => {
afterEach(() => {
uaConfig.defaultRecordingRulesTargetDatasourceUID = undefined;
jest.clearAllMocks();
});
it('should set targetDatasourceUid from config when datasource is valid for recording rules', () => {
const expectedDatasourceUid = 'test-datasource-uid';
uaConfig.defaultRecordingRulesTargetDatasourceUID = expectedDatasourceUid;
const validDataSource = mockDataSource({
uid: expectedDatasourceUid,
type: DataSourceType.Prometheus,
jsonData: {
allowAsRecordingRulesTarget: true,
},
});
mockGetInstanceSettings.mockReturnValue(validDataSource);
const result = getDefaultFormValues();
expect(result.targetDatasourceUid).toBe(expectedDatasourceUid);
expect(mockGetInstanceSettings).toHaveBeenCalledWith(expectedDatasourceUid);
});
it('should set targetDatasourceUid to undefined when datasource has allowAsRecordingRulesTarget disabled', () => {
const datasourceUid = 'test-datasource-uid';
uaConfig.defaultRecordingRulesTargetDatasourceUID = datasourceUid;
const invalidDataSource = mockDataSource({
uid: datasourceUid,
type: DataSourceType.Prometheus,
jsonData: {
allowAsRecordingRulesTarget: false,
},
});
mockGetInstanceSettings.mockReturnValue(invalidDataSource);
const result = getDefaultFormValues();
expect(result.targetDatasourceUid).toBeUndefined();
expect(mockGetInstanceSettings).toHaveBeenCalledWith(datasourceUid);
});
it('should set targetDatasourceUid to undefined when datasource type is not supported', () => {
const datasourceUid = 'test-datasource-uid';
uaConfig.defaultRecordingRulesTargetDatasourceUID = datasourceUid;
const nonPrometheusDataSource = mockDataSource({
uid: datasourceUid,
type: DataSourceType.Loki,
jsonData: {
allowAsRecordingRulesTarget: true,
},
});
mockGetInstanceSettings.mockReturnValue(nonPrometheusDataSource);
const result = getDefaultFormValues();
expect(result.targetDatasourceUid).toBeUndefined();
expect(mockGetInstanceSettings).toHaveBeenCalledWith(datasourceUid);
});
it('should set targetDatasourceUid to undefined when datasource does not exist', () => {
const datasourceUid = 'non-existent-datasource-uid';
uaConfig.defaultRecordingRulesTargetDatasourceUID = datasourceUid;
mockGetInstanceSettings.mockReturnValue(null);
const result = getDefaultFormValues();
expect(result.targetDatasourceUid).toBeUndefined();
expect(mockGetInstanceSettings).toHaveBeenCalledWith(datasourceUid);
});
it('should set targetDatasourceUid to undefined when defaultRecordingRulesTargetDatasourceUID is not provided', () => {
const result = getDefaultFormValues();
expect(result.targetDatasourceUid).toBeUndefined();
expect(mockGetInstanceSettings).not.toHaveBeenCalled();
});
});
@@ -1,7 +1,7 @@
import { clamp } from 'lodash';
import { z } from 'zod/v4';
import { config } from '@grafana/runtime';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { RuleWithLocation } from 'app/types/unified-alerting';
import { GrafanaAlertStateDecision, RulerRuleDTO } from 'app/types/unified-alerting-dto';
@@ -9,7 +9,7 @@ import { RuleFormType, RuleFormValues } from '../types/rule-form';
// TODO Ideally all of these should be moved here
import { getRulesAccess } from '../utils/access-control';
import { defaultAnnotations } from '../utils/constants';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { GRAFANA_RULES_SOURCE_NAME, isValidRecordingRulesTarget } from '../utils/datasource';
import {
MANUAL_ROUTING_KEY,
SIMPLIFIED_QUERY_EDITOR_KEY,
@@ -35,6 +35,27 @@ const KEEP_FIRING_FOR_DEFAULT = '0s';
export const DEFAULT_GROUP_EVALUATION_INTERVAL = formatPrometheusDuration(
clamp(GROUP_EVALUATION_MIN_INTERVAL_MS, GROUP_EVALUATION_INTERVAL_LOWER_BOUND, GROUP_EVALUATION_INTERVAL_UPPER_BOUND)
);
function getValidDefaultTargetDatasourceUid(): string | undefined {
const configuredDefaultUid = config.unifiedAlerting?.defaultRecordingRulesTargetDatasourceUID;
if (!configuredDefaultUid) {
return undefined;
}
try {
const datasource = getDataSourceSrv().getInstanceSettings(configuredDefaultUid);
if (datasource && isValidRecordingRulesTarget(datasource)) {
return configuredDefaultUid;
}
} catch (error) {
// If datasource doesn't exist or can't be retrieved,
// just return undefined
}
return undefined;
}
export const getDefaultFormValues = (ruleType?: RuleFormType): RuleFormValues => {
const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess();
const type = (() => {
@@ -75,7 +96,7 @@ export const getDefaultFormValues = (ruleType?: RuleFormType): RuleFormValues =>
overrideTimings: false,
muteTimeIntervals: [],
editorSettings: getDefaultEditorSettings(ruleType),
targetDatasourceUid: config.unifiedAlerting?.defaultRecordingRulesTargetDatasourceUID,
targetDatasourceUid: getValidDefaultTargetDatasourceUid(),
// cortex / loki
namespace: '',