Fix ROUTES_META_SYMBOL being lost because of RTK query cache transform

This commit is contained in:
Matt Jacobson
2026-01-07 12:00:49 -05:00
parent 589b359fef
commit 78f8acb056
4 changed files with 99 additions and 206 deletions
@@ -1,89 +0,0 @@
import { MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route } from '../openapi/routesApi.gen';
import { k8sSubRouteToRoute, routeToK8sSubRoute } from './routesApi';
test('k8sSubRouteToRoute', () => {
const input: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = {
continue: false,
group_by: ['label1'],
group_interval: '5m',
group_wait: '30s',
matchers: [{ label: 'label1', type: '=', value: 'value1' }],
mute_time_intervals: ['mt-1'],
receiver: 'my-receiver',
repeat_interval: '4h',
routes: [
{
receiver: 'receiver2',
matchers: [{ label: 'label2', type: '!=', value: 'value2' }],
},
],
};
const expected: Route = {
name: 'test-name',
continue: false,
group_by: ['label1'],
group_interval: '5m',
group_wait: '30s',
matchers: undefined, // matchers -> object_matchers
object_matchers: [['label1', MatcherOperator.equal, 'value1']],
mute_time_intervals: ['mt-1'],
receiver: 'my-receiver',
repeat_interval: '4h',
routes: [
{
name: 'test-name',
receiver: 'receiver2',
matchers: undefined,
object_matchers: [['label2', MatcherOperator.notEqual, 'value2']],
routes: undefined,
},
],
};
expect(k8sSubRouteToRoute(input, 'test-name')).toStrictEqual(expected);
});
test('routeToK8sSubRoute', () => {
const input: Route = {
continue: false,
group_by: ['label1'],
group_interval: '5m',
group_wait: '30s',
matchers: undefined, // matchers -> object_matchers
object_matchers: [['label1', MatcherOperator.equal, 'value1']],
mute_time_intervals: ['mt-1'],
receiver: 'my-receiver',
repeat_interval: '4h',
routes: [
{
receiver: 'receiver2',
matchers: undefined,
object_matchers: [['label2', MatcherOperator.notEqual, 'value2']],
},
],
};
const expected: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route = {
continue: false,
group_by: ['label1'],
group_interval: '5m',
group_wait: '30s',
matchers: [{ label: 'label1', type: '=', value: 'value1' }],
mute_time_intervals: ['mt-1'],
receiver: 'my-receiver',
repeat_interval: '4h',
routes: [
{
receiver: 'receiver2',
matchers: [{ label: 'label2', type: '!=', value: 'value2' }],
routes: undefined,
},
],
};
expect(routeToK8sSubRoute(input)).toStrictEqual(expected);
});
@@ -1,101 +0,0 @@
import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree,
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
generatedRoutesApi,
ReadNamespacedRoutingTreeApiResponse,
ListNamespacedRoutingTreeApiResponse,
} from 'app/features/alerting/unified/openapi/routesApi.gen';
import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types';
import { ROOT_ROUTE_NAME } from '../utils/k8s/constants';
import { isK8sEntityProvisioned } from '../utils/k8s/utils';
import { DefinitionsFromApi, OverrideResultType, TagTypesFromApi } from '@reduxjs/toolkit/query';
type Definitions = DefinitionsFromApi<typeof generatedRoutesApi>;
type TagTypes = TagTypesFromApi<typeof generatedRoutesApi>;
type UpdatedDefinitions = Omit<Definitions, 'readNamespacedRoutingTree' | 'listNamespacedRoutingTree'> & {
readNamespacedRoutingTree: OverrideResultType<Definitions['readNamespacedRoutingTree'], Route>;
listNamespacedRoutingTree: OverrideResultType<Definitions['listNamespacedRoutingTree'], Route[]>;
};
export const routesApi = generatedRoutesApi.enhanceEndpoints<TagTypes, UpdatedDefinitions>({
endpoints: {
readNamespacedRoutingTree: (endpoint) => {
// We transform the response here instead of in `selectFromResult` so that memoization of the transformed Route
// is automatically handled.
endpoint.transformResponse = (response: ReadNamespacedRoutingTreeApiResponse): Route => {
return k8sRouteToRoute(response);
};
},
listNamespacedRoutingTree: (endpoint) => {
endpoint.transformResponse = (response: ListNamespacedRoutingTreeApiResponse): Route[] => {
return k8sRoutesToRoutes(response.items);
};
},
},
});
export const NAMED_ROOT_LABEL_NAME = '__grafana_managed_route__';
function k8sRouteToRoute(route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree): Route {
return {
...route.spec.defaults,
name: route.metadata.name,
routes: route.spec.routes?.map((subroute) => k8sSubRouteToRoute(subroute, route.metadata.name)),
// This assumes if a `NAMED_ROOT_LABEL_NAME` label exists, it will NOT go to the default route, which is a fair but
// not perfect assumption since we don't yet protect the label.
object_matchers:
route.metadata.name == ROOT_ROUTE_NAME || !route.metadata.name
? [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, '']]
: [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, route.metadata.name]],
[ROUTES_META_SYMBOL]: {
provisioned: isK8sEntityProvisioned(route),
resourceVersion: route.metadata.resourceVersion,
name: route.metadata.name,
metadata: route.metadata,
},
};
}
function k8sRoutesToRoutes(routes: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree[]): Route[] {
return routes?.map((route) => {
return k8sRouteToRoute(route);
});
}
/** Helper to provide type safety for matcher operators from API */
function isValidMatcherOperator(type: string): type is MatcherOperator {
return Object.values<string>(MatcherOperator).includes(type);
}
export function k8sSubRouteToRoute(
route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
rootName?: string
): Route {
return {
...route,
name: rootName,
routes: route.routes?.map((subroute) => k8sSubRouteToRoute(subroute, rootName)),
matchers: undefined,
object_matchers: route.matchers?.map(({ label, type, value }) => {
if (!isValidMatcherOperator(type)) {
throw new Error(`Invalid matcher operator from API: ${type}`);
}
return [label, type, value];
}),
};
}
export function routeToK8sSubRoute(route: Route): ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route {
const { object_matchers, ...rest } = route;
return {
...rest,
receiver: route.receiver ?? undefined,
matchers: object_matchers?.map(([label, type, value]) => ({
label,
type,
value,
})),
routes: route.routes?.map(routeToK8sSubRoute),
};
}
@@ -3,7 +3,7 @@ import memoize from 'micro-memoize';
import { INHERITABLE_KEYS, type InheritableProperties } from '@grafana/alerting/internal';
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { ROUTES_META_SYMBOL, Route, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
import { ROUTES_META_SYMBOL, Route, RouteWithID, MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
import { getAPINamespace } from '../../../../../api/utils';
import { alertmanagerApi } from '../../api/alertmanagerApi';
@@ -12,8 +12,9 @@ import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNe
import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RouteDefaults,
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree,
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
generatedRoutesApi as routingTreeApi,
} from '../../openapi/routesApi.gen';
import { routesApi as routingTreeApi, routeToK8sSubRoute } from '../../api/routesApi';
import {
addRouteAction,
deleteRouteAction,
@@ -22,7 +23,7 @@ import {
import { FormAmRoute } from '../../types/amroutes';
import { addUniqueIdentifierToRoute } from '../../utils/amroutes';
import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
import { shouldUseK8sApi } from '../../utils/k8s/utils';
import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils';
import { routeAdapter } from '../../utils/routeAdapter';
import {
InsertPosition,
@@ -57,11 +58,21 @@ export const useNotificationPolicyRoute = (
{
skip: skip || !k8sApiSupported,
selectFromResult: (result) => {
return {
...result,
currentData: result.currentData,
data: result.data,
};
const { data, currentData, ...rest } = result;
const transformed = useMemo(() => {
return data ? k8sRouteToRoute(data) : data;
}, [data]);
const transformedCurrent = useMemo(() => {
return currentData ? k8sRouteToRoute(currentData) : currentData;
}, [currentData]);
return {
...rest,
data: transformed,
currentData: transformedCurrent,
};
},
}
);
@@ -90,10 +101,20 @@ export const useListNotificationPolicyRoutes = ({ skip }: Skippable = {}) => {
{
skip: skip,
selectFromResult: (result) => {
const { data, currentData, ...rest } = result;
const transformed = useMemo(() => {
return data ? data.items.map(k8sRouteToRoute) : data;
}, [data]);
const transformedCurrent = useMemo(() => {
return currentData ? currentData.items.map(k8sRouteToRoute) : currentData;
}, [currentData]);
return {
...result,
currentData: result.currentData,
data: result.data,
...rest,
data: transformed,
currentData: transformedCurrent,
};
},
}
@@ -125,7 +146,7 @@ export function useUpdateExistingNotificationPolicy({ alertmanager }: BaseAlertm
throw new Error(`no root route found for namespace ${namespace} and name ${name}`);
}
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree);
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(k8sRouteToRoute(rootTree));
const newRouteTree = mergePartialAmRouteWithRouteTree(alertmanager, update, rootRouteWithIdentifiers);
// Create the K8s route object
@@ -162,7 +183,7 @@ export function useDeleteNotificationPolicy({ alertmanager }: BaseAlertmanagerAr
throw new Error(`no root route found for namespace ${namespace}`);
}
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree);
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(k8sRouteToRoute(rootTree));
const newRouteTree = omitRouteFromRouteTree(route.id, rootRouteWithIdentifiers);
// Create the K8s route object
@@ -208,7 +229,7 @@ export function useAddNotificationPolicy({ alertmanager }: BaseAlertmanagerArgs)
throw new Error(`no root route found for namespace ${namespace}`);
}
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree);
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(k8sRouteToRoute(rootTree));
const newRouteTree = addRouteToReferenceRoute(
alertmanager ?? '',
partialRoute,
@@ -369,3 +390,63 @@ export function createKubernetesRoutingTreeSpec(
},
};
}
export const NAMED_ROOT_LABEL_NAME = '__grafana_managed_route__';
function k8sRouteToRoute(route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree): Route {
return {
...route.spec.defaults,
name: route.metadata.name,
routes: route.spec.routes?.map((subroute) => k8sSubRouteToRoute(subroute, route.metadata.name)),
// This assumes if a `NAMED_ROOT_LABEL_NAME` label exists, it will NOT go to the default route, which is a fair but
// not perfect assumption since we don't yet protect the label.
object_matchers:
route.metadata.name == ROOT_ROUTE_NAME || !route.metadata.name
? [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, '']]
: [[NAMED_ROOT_LABEL_NAME, MatcherOperator.equal, route.metadata.name]],
[ROUTES_META_SYMBOL]: {
provisioned: isK8sEntityProvisioned(route),
resourceVersion: route.metadata.resourceVersion,
name: route.metadata.name,
metadata: route.metadata,
},
};
}
/** Helper to provide type safety for matcher operators from API */
function isValidMatcherOperator(type: string): type is MatcherOperator {
return Object.values<string>(MatcherOperator).includes(type);
}
export function k8sSubRouteToRoute(
route: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
rootName?: string
): Route {
return {
...route,
name: rootName,
routes: route.routes?.map((subroute) => k8sSubRouteToRoute(subroute, rootName)),
matchers: undefined,
object_matchers: route.matchers?.map(({ label, type, value }) => {
if (!isValidMatcherOperator(type)) {
throw new Error(`Invalid matcher operator from API: ${type}`);
}
return [label, type, value];
}),
};
}
export function routeToK8sSubRoute(route: Route): ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route {
const { object_matchers, ...rest } = route;
return {
...rest,
receiver: route.receiver ?? undefined,
matchers: object_matchers?.map(([label, type, value]) => ({
label,
type,
value,
})),
routes: route.routes?.map(routeToK8sSubRoute),
};
}
@@ -1,14 +1,16 @@
import { useMemo } from 'react';
import { useAsync } from 'react-use';
import { useNotificationPolicyRoute } from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute';
import {
NAMED_ROOT_LABEL_NAME,
useNotificationPolicyRoute,
} from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute';
import { Labels } from '../../../../../../types/unified-alerting-dto';
import { useRouteGroupsMatcher } from '../../../useRouteGroupsMatcher';
import { addUniqueIdentifierToRoute } from '../../../utils/amroutes';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
import { normalizeRoute } from '../../../utils/notification-policies';
import { NAMED_ROOT_LABEL_NAME } from '../../../api/routesApi';
export const useAlertmanagerNotificationRoutingPreview = (alertmanager: string, instances: Labels[]) => {
// if a NAMED_ROOT_LABEL_NAME label exists, then we only match to that route.