Alerting: Allow disabling recording rules write for a data source in the UI (#106664)

Adds a new "Allow as recording rules target" toggle to Prometheus datasource configuration that controls whether the datasource can be selected as a target for writing recording rules.
---------

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
Alexander Akhmetov
2025-06-17 22:55:45 +02:00
committed by GitHub
parent 6cb2c701e6
commit f6e330b3d7
19 changed files with 201 additions and 20 deletions

View File

@ -25,7 +25,11 @@ import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/Neste
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { Folder } from '../../types/rule-form';
import { DataSourceType } from '../../utils/datasource';
import {
DataSourceType,
isSupportedExternalPrometheusFlavoredRulesSourceType,
isValidRecordingRulesTarget,
} from '../../utils/datasource';
import { stringifyErrorLike } from '../../utils/misc';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertingPageWrapper } from '../AlertingPageWrapper';
@ -319,11 +323,11 @@ function YamlTargetDataSourceField() {
noDefault
inputId="yaml-target-data-source"
alerting
filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'}
filter={(ds: DataSourceInstanceSettings) => isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type)}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('yamlImportTargetDatasourceUID', ds.uid);
const recordingRulesTargetDs = getValues('targetDatasourceUID');
if (!recordingRulesTargetDs) {
if (!recordingRulesTargetDs && isValidRecordingRulesTarget(ds)) {
setValue('targetDatasourceUID', ds.uid);
}
}}
@ -367,7 +371,7 @@ function TargetDataSourceForRecordingRulesField() {
current={field.value}
inputId="recording-rules-target-data-source"
noDefault
filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'}
filter={isValidRecordingRulesTarget}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('targetDatasourceUID', ds.uid);
}}
@ -479,7 +483,7 @@ function DataSourceField() {
// If we've chosen a Prometheus data source, we can set the recording rules target data source to the same as the source
const recordingRulesTargetDs = getValues('targetDatasourceUID');
if (!recordingRulesTargetDs) {
const targetDataSourceUID = ds.type === DataSourceType.Prometheus ? ds.uid : undefined;
const targetDataSourceUID = isValidRecordingRulesTarget(ds) ? ds.uid : undefined;
setValue('targetDatasourceUID', targetDataSourceUID);
}
}}

View File

@ -7,7 +7,7 @@ import { Field, Input, Stack, Text } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { isSupportedExternalPrometheusFlavoredRulesSourceType } from '../../utils/datasource';
import { isValidRecordingRulesTarget } from '../../utils/datasource';
import { isCloudRecordingRuleByType, isGrafanaRecordingRuleByType, isRecordingRuleByType } from '../../utils/rules';
import { RuleEditorSection } from './RuleEditorSection';
@ -128,9 +128,7 @@ export const AlertRuleNameAndMetric = () => {
current={field.value}
noDefault
// Filter with `filter` prop instead of `type` prop to avoid showing the `-- Grafana --` data source
filter={(ds: DataSourceInstanceSettings) =>
isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type)
}
filter={isValidRecordingRulesTarget}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('targetDatasourceUid', ds.uid);
}}

View File

@ -1,6 +1,10 @@
import { mockDataSource } from '../mocks';
import { isDataSourceManagingAlerts } from './datasource';
import {
SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES,
isDataSourceManagingAlerts,
isValidRecordingRulesTarget,
} from './datasource';
describe('isDataSourceManagingAlerts', () => {
it('should return true when the prop is set as true', () => {
@ -37,3 +41,48 @@ it('should return false when the prop is set as false', () => {
)
).toBe(false);
});
describe('isValidRecordingRulesTarget', () => {
it.each(SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES)(
'should return true for %s datasource with manageRecordingRulesTarget enabled',
(type) => {
expect(
isValidRecordingRulesTarget(
mockDataSource({
type,
jsonData: {
allowAsRecordingRulesTarget: true,
},
})
)
).toBe(true);
}
);
it.each(SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES)(
'should return true for %s datasource when manageRecordingRulesTarget is undefined (defaults to true)',
(type) => {
expect(
isValidRecordingRulesTarget(
mockDataSource({
type,
jsonData: {},
})
)
).toBe(true);
}
);
it('should return false for loki datasource (unsupported type)', () => {
expect(
isValidRecordingRulesTarget(
mockDataSource({
type: 'loki',
jsonData: {
allowAsRecordingRulesTarget: true,
},
})
)
).toBe(false);
});
});

View File

@ -338,6 +338,10 @@ export function isDataSourceManagingAlerts(ds: DataSourceInstanceSettings<DataSo
return ds.jsonData.manageAlerts !== false; //if this prop is undefined it defaults to true
}
export function isDataSourceAllowedAsRecordingRulesTarget(ds: DataSourceInstanceSettings<DataSourceJsonData>) {
return ds.jsonData.allowAsRecordingRulesTarget !== false; // if this prop is undefined it defaults to true
}
export function ruleIdentifierToRuleSourceIdentifier(ruleIdentifier: RuleIdentifier): RulesSourceIdentifier {
if (isGrafanaRuleIdentifier(ruleIdentifier)) {
return { uid: GrafanaRulesSourceSymbol, name: GRAFANA_RULES_SOURCE_NAME, ruleSourceType: 'grafana' };
@ -389,3 +393,7 @@ export const SUPPORTED_RULE_SOURCE_TYPES = [
GRAFANA_RULES_SOURCE_NAME,
...SUPPORTED_EXTERNAL_RULE_SOURCE_TYPES,
] as const satisfies string[];
export function isValidRecordingRulesTarget(ds: DataSourceInstanceSettings<DataSourceJsonData>): boolean {
return isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type) && isDataSourceAllowedAsRecordingRulesTarget(ds);
}