2025-04-01 10:38:02 +09:00

305 lines
11 KiB
TypeScript

import { pick } from 'lodash';
import memoize from 'micro-memoize';
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
import { MatcherOperator, ROUTES_META_SYMBOL, Route } from 'app/plugins/datasource/alertmanager/types';
import { getAPINamespace } from '../../../../../api/utils';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { useAsync } from '../../hooks/useAsync';
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Route,
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RouteDefaults,
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree,
generatedRoutesApi as routingTreeApi,
} from '../../openapi/routesApi.gen';
import {
addRouteAction,
deleteRouteAction,
updateRouteAction,
} from '../../reducers/alertmanager/notificationPolicyRoutes';
import { FormAmRoute } from '../../types/amroutes';
import { addUniqueIdentifierToRoute } from '../../utils/amroutes';
import { PROVENANCE_NONE, ROOT_ROUTE_NAME } from '../../utils/k8s/constants';
import { isK8sEntityProvisioned, shouldUseK8sApi } from '../../utils/k8s/utils';
import { INHERITABLE_KEYS, InheritableProperties } from '../../utils/notification-policies';
import {
InsertPosition,
addRouteToReferenceRoute,
cleanKubernetesRouteIDs,
mergePartialAmRouteWithRouteTree,
omitRouteFromRouteTree,
} from '../../utils/routeTree';
const k8sRoutesToRoutesMemoized = memoize(k8sRoutesToRoutes, { maxSize: 1 });
const {
useListNamespacedRoutingTreeQuery,
useReplaceNamespacedRoutingTreeMutation,
useLazyListNamespacedRoutingTreeQuery,
} = routingTreeApi;
const { useGetAlertmanagerConfigurationQuery } = alertmanagerApi;
export const useNotificationPolicyRoute = ({ alertmanager }: BaseAlertmanagerArgs, { skip }: Skippable = {}) => {
const k8sApiSupported = shouldUseK8sApi(alertmanager);
const k8sRouteQuery = useListNamespacedRoutingTreeQuery(
{ namespace: getAPINamespace() },
{
skip: skip || !k8sApiSupported,
selectFromResult: (result) => {
return {
...result,
currentData: result.currentData ? k8sRoutesToRoutesMemoized(result.currentData.items) : undefined,
data: result.data ? k8sRoutesToRoutesMemoized(result.data.items) : undefined,
};
},
}
);
const amConfigQuery = useGetAlertmanagerConfigurationQuery(alertmanager, {
skip: skip || k8sApiSupported,
selectFromResult: (result) => {
return {
...result,
currentData: result.currentData?.alertmanager_config?.route
? [parseAmConfigRoute(result.currentData.alertmanager_config.route)]
: undefined,
data: result.data?.alertmanager_config?.route
? [parseAmConfigRoute(result.data.alertmanager_config.route)]
: undefined,
};
},
});
return k8sApiSupported ? k8sRouteQuery : amConfigQuery;
};
const parseAmConfigRoute = memoize((route: Route): Route => {
return {
...route,
[ROUTES_META_SYMBOL]: {
provisioned: Boolean(route.provenance && route.provenance !== PROVENANCE_NONE),
},
};
});
export function useUpdateExistingNotificationPolicy({ alertmanager }: BaseAlertmanagerArgs) {
const k8sApiSupported = shouldUseK8sApi(alertmanager);
const [updatedNamespacedRoute] = useReplaceNamespacedRoutingTreeMutation();
const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
const [listNamespacedRoutingTree] = useLazyListNamespacedRoutingTreeQuery();
const updateUsingK8sApi = useAsync(async (update: Partial<FormAmRoute>) => {
const namespace = getAPINamespace();
const result = await listNamespacedRoutingTree({ namespace });
const [rootTree] = result.data ? k8sRoutesToRoutesMemoized(result.data.items) : [];
if (!rootTree) {
throw new Error(`no root route found for namespace ${namespace}`);
}
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree);
const newRouteTree = mergePartialAmRouteWithRouteTree(alertmanager, update, rootRouteWithIdentifiers);
// Create the K8s route object
const routeObject = createKubernetesRoutingTreeSpec(newRouteTree);
return updatedNamespacedRoute({
name: ROOT_ROUTE_NAME,
namespace,
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree: cleanKubernetesRouteIDs(routeObject),
}).unwrap();
});
const updateFromAlertmanagerConfiguration = useAsync(async (update: Partial<FormAmRoute>) => {
const action = updateRouteAction({ update, alertmanager });
return produceNewAlertmanagerConfiguration(action);
});
return k8sApiSupported ? updateUsingK8sApi : updateFromAlertmanagerConfiguration;
}
export function useDeleteNotificationPolicy({ alertmanager }: BaseAlertmanagerArgs) {
const k8sApiSupported = shouldUseK8sApi(alertmanager);
const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
const [listNamespacedRoutingTree] = useLazyListNamespacedRoutingTreeQuery();
const [updatedNamespacedRoute] = useReplaceNamespacedRoutingTreeMutation();
const deleteFromK8sApi = useAsync(async (id: string) => {
const namespace = getAPINamespace();
const result = await listNamespacedRoutingTree({ namespace });
const [rootTree] = result.data ? k8sRoutesToRoutesMemoized(result.data.items) : [];
if (!rootTree) {
throw new Error(`no root route found for namespace ${namespace}`);
}
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree);
const newRouteTree = omitRouteFromRouteTree(id, rootRouteWithIdentifiers);
// Create the K8s route object
const routeObject = createKubernetesRoutingTreeSpec(newRouteTree);
return updatedNamespacedRoute({
name: ROOT_ROUTE_NAME,
namespace,
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree: routeObject,
}).unwrap();
});
const deleteFromAlertmanagerConfiguration = useAsync(async (id: string) => {
const action = deleteRouteAction({ id });
return produceNewAlertmanagerConfiguration(action);
});
return k8sApiSupported ? deleteFromK8sApi : deleteFromAlertmanagerConfiguration;
}
export function useAddNotificationPolicy({ alertmanager }: BaseAlertmanagerArgs) {
const k8sApiSupported = shouldUseK8sApi(alertmanager);
const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
const [listNamespacedRoutingTree] = useLazyListNamespacedRoutingTreeQuery();
const [updatedNamespacedRoute] = useReplaceNamespacedRoutingTreeMutation();
const addToK8sApi = useAsync(
async ({
partialRoute,
referenceRouteIdentifier,
insertPosition,
}: {
partialRoute: Partial<FormAmRoute>;
referenceRouteIdentifier: string;
insertPosition: InsertPosition;
}) => {
const namespace = getAPINamespace();
const result = await listNamespacedRoutingTree({ namespace });
const [rootTree] = result.data ? k8sRoutesToRoutesMemoized(result.data.items) : [];
if (!rootTree) {
throw new Error(`no root route found for namespace ${namespace}`);
}
const rootRouteWithIdentifiers = addUniqueIdentifierToRoute(rootTree);
const newRouteTree = addRouteToReferenceRoute(
alertmanager ?? '',
partialRoute,
referenceRouteIdentifier,
rootRouteWithIdentifiers,
insertPosition
);
// Create the K8s route object
const routeObject = createKubernetesRoutingTreeSpec(newRouteTree);
return updatedNamespacedRoute({
name: ROOT_ROUTE_NAME,
namespace,
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree: cleanKubernetesRouteIDs(routeObject),
}).unwrap();
}
);
const addToAlertmanagerConfiguration = useAsync(
async ({
partialRoute,
referenceRouteIdentifier,
insertPosition,
}: {
partialRoute: Partial<FormAmRoute>;
referenceRouteIdentifier: string;
insertPosition: InsertPosition;
}) => {
const action = addRouteAction({
partialRoute,
referenceRouteIdentifier,
insertPosition,
alertmanager,
});
return produceNewAlertmanagerConfiguration(action);
}
);
return k8sApiSupported ? addToK8sApi : addToAlertmanagerConfiguration;
}
function k8sRoutesToRoutes(routes: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree[]): Route[] {
return routes?.map((route) => {
return {
...route.spec.defaults,
routes: route.spec.routes?.map(k8sSubRouteToRoute),
[ROUTES_META_SYMBOL]: {
provisioned: isK8sEntityProvisioned(route),
resourceVersion: route.metadata.resourceVersion,
name: route.metadata.name,
},
};
});
}
/** 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): Route {
return {
...route,
routes: route.routes?.map(k8sSubRouteToRoute),
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),
};
}
/**
* Convert Route to K8s compatible format. Make sure we aren't sending any additional properties the API doesn't recognize
* because it will reply with excess properties in the HTTP headers
*/
export function createKubernetesRoutingTreeSpec(
rootRoute: Route
): ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RoutingTree {
const inheritableDefaultProperties: InheritableProperties = pick(rootRoute, INHERITABLE_KEYS);
const defaults: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1RouteDefaults = {
...inheritableDefaultProperties,
// TODO: Fix types in k8s API? Fix our types to not allow empty receiver? TBC
receiver: rootRoute.receiver ?? '',
};
const routes = rootRoute.routes?.map(routeToK8sSubRoute) ?? [];
const spec = {
defaults,
routes,
};
return {
spec: spec,
metadata: {
name: ROOT_ROUTE_NAME,
resourceVersion: rootRoute[ROUTES_META_SYMBOL]?.resourceVersion,
},
};
}