mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Use useProduceNewAlertmanagerConfiguration for contact points (#88456)
This commit is contained in:
parent
02f608ed05
commit
41b175e7ae
@ -22,16 +22,15 @@ import { RECEIVER_META_KEY, RECEIVER_PLUGIN_META_KEY, RECEIVER_STATUS_KEY } from
|
|||||||
import { ContactPointWithMetadata, getReceiverDescription, ReceiverConfigWithMetadata } from './utils';
|
import { ContactPointWithMetadata, getReceiverDescription, ReceiverConfigWithMetadata } from './utils';
|
||||||
|
|
||||||
interface ContactPointProps {
|
interface ContactPointProps {
|
||||||
disabled?: boolean;
|
|
||||||
contactPoint: ContactPointWithMetadata;
|
contactPoint: ContactPointWithMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactPoint = ({ disabled = false, contactPoint }: ContactPointProps) => {
|
export const ContactPoint = ({ contactPoint }: ContactPointProps) => {
|
||||||
const { grafana_managed_receiver_configs: receivers } = contactPoint;
|
const { grafana_managed_receiver_configs: receivers } = contactPoint;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { selectedAlertmanager } = useAlertmanager();
|
const { selectedAlertmanager } = useAlertmanager();
|
||||||
const handleDelete = useDeleteContactPoint({ alertmanager: selectedAlertmanager! });
|
const [deleteTrigger] = useDeleteContactPoint({ alertmanager: selectedAlertmanager! });
|
||||||
const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(handleDelete);
|
const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger.execute);
|
||||||
|
|
||||||
// TODO probably not the best way to figure out if we want to show either only the summary or full metadata for the receivers?
|
// TODO probably not the best way to figure out if we want to show either only the summary or full metadata for the receivers?
|
||||||
const showFullMetadata = receivers.some((receiver) => Boolean(receiver[RECEIVER_META_KEY]));
|
const showFullMetadata = receivers.some((receiver) => Boolean(receiver[RECEIVER_META_KEY]));
|
||||||
@ -41,7 +40,6 @@ export const ContactPoint = ({ disabled = false, contactPoint }: ContactPointPro
|
|||||||
<Stack direction="column" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<ContactPointHeader
|
<ContactPointHeader
|
||||||
contactPoint={contactPoint}
|
contactPoint={contactPoint}
|
||||||
disabled={disabled}
|
|
||||||
onDelete={(contactPointToDelete) =>
|
onDelete={(contactPointToDelete) =>
|
||||||
showDeleteModal({
|
showDeleteModal({
|
||||||
name: contactPointToDelete.id || contactPointToDelete.name,
|
name: contactPointToDelete.id || contactPointToDelete.name,
|
||||||
@ -64,6 +62,7 @@ export const ContactPoint = ({ disabled = false, contactPoint }: ContactPointPro
|
|||||||
const sendingResolved = !Boolean(receiver.disableResolveMessage);
|
const sendingResolved = !Boolean(receiver.disableResolveMessage);
|
||||||
const pluginMetadata = receiver[RECEIVER_PLUGIN_META_KEY];
|
const pluginMetadata = receiver[RECEIVER_PLUGIN_META_KEY];
|
||||||
const key = metadata.name + index;
|
const key = metadata.name + index;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContactPointReceiver
|
<ContactPointReceiver
|
||||||
key={key}
|
key={key}
|
||||||
|
@ -27,11 +27,10 @@ import { ContactPointWithMetadata, showManageContactPointPermissions } from './u
|
|||||||
|
|
||||||
interface ContactPointHeaderProps {
|
interface ContactPointHeaderProps {
|
||||||
contactPoint: ContactPointWithMetadata;
|
contactPoint: ContactPointWithMetadata;
|
||||||
disabled?: boolean;
|
|
||||||
onDelete: (contactPoint: ContactPointWithMetadata) => void;
|
onDelete: (contactPoint: ContactPointWithMetadata) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactPointHeader = ({ contactPoint, disabled = false, onDelete }: ContactPointHeaderProps) => {
|
export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeaderProps) => {
|
||||||
const { name, id, provisioned, policies = [] } = contactPoint;
|
const { name, id, provisioned, policies = [] } = contactPoint;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
const [showPermissionsDrawer, setShowPermissionsDrawer] = useState(false);
|
||||||
@ -160,7 +159,7 @@ export const ContactPointHeader = ({ contactPoint, disabled = false, onDelete }:
|
|||||||
ariaLabel="delete"
|
ariaLabel="delete"
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
destructive
|
destructive
|
||||||
disabled={disabled || !canBeDeleted}
|
disabled={!canBeDeleted}
|
||||||
onClick={() => onDelete(contactPoint)}
|
onClick={() => onDelete(contactPoint)}
|
||||||
/>
|
/>
|
||||||
</ConditionalWrap>
|
</ConditionalWrap>
|
||||||
@ -219,7 +218,6 @@ export const ContactPointHeader = ({ contactPoint, disabled = false, onDelete }:
|
|||||||
size="sm"
|
size="sm"
|
||||||
icon={canEdit ? 'pen' : 'eye'}
|
icon={canEdit ? 'pen' : 'eye'}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
|
||||||
aria-label={`${canEdit ? 'edit' : 'view'}-action`}
|
aria-label={`${canEdit ? 'edit' : 'view'}-action`}
|
||||||
data-testid={`${canEdit ? 'edit' : 'view'}-action`}
|
data-testid={`${canEdit ? 'edit' : 'view'}-action`}
|
||||||
href={`/alerting/notifications/receivers/${encodeURIComponent(urlId)}/edit`}
|
href={`/alerting/notifications/receivers/${encodeURIComponent(urlId)}/edit`}
|
||||||
|
@ -4,9 +4,9 @@ import { render, screen, userEvent, waitFor, waitForElementToBeRemoved, within }
|
|||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import {
|
import {
|
||||||
|
flushMicrotasks,
|
||||||
testWithFeatureToggles,
|
testWithFeatureToggles,
|
||||||
testWithLicenseFeatures,
|
testWithLicenseFeatures,
|
||||||
flushMicrotasks,
|
|
||||||
} from 'app/features/alerting/unified/test/test-utils';
|
} from 'app/features/alerting/unified/test/test-utils';
|
||||||
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
import { K8sAnnotations } from 'app/features/alerting/unified/utils/k8s/constants';
|
||||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||||
@ -45,7 +45,7 @@ import { ContactPointWithMetadata, ReceiverConfigWithMetadata, RouteReference }
|
|||||||
*/
|
*/
|
||||||
const server = setupMswServer();
|
const server = setupMswServer();
|
||||||
|
|
||||||
const renderWithProvider = (
|
export const renderWithProvider = (
|
||||||
children: ReactNode,
|
children: ReactNode,
|
||||||
historyOptions?: MemoryHistoryBuildOptions,
|
historyOptions?: MemoryHistoryBuildOptions,
|
||||||
providerProps?: Partial<ComponentProps<typeof AlertmanagerProvider>>
|
providerProps?: Partial<ComponentProps<typeof AlertmanagerProvider>>
|
||||||
@ -213,16 +213,6 @@ describe('contact points', () => {
|
|||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable edit button', async () => {
|
|
||||||
renderWithProvider(<ContactPoint contactPoint={basicContactPoint} disabled={true} />);
|
|
||||||
|
|
||||||
const moreActions = screen.getByRole('button', { name: /More/ });
|
|
||||||
expect(moreActions).toBeEnabled();
|
|
||||||
|
|
||||||
const editAction = screen.getByTestId('edit-action');
|
|
||||||
expect(editAction).toHaveAttribute('aria-disabled', 'true');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show warning when no receivers are configured', async () => {
|
it('should show warning when no receivers are configured', async () => {
|
||||||
renderWithProvider(<ContactPoint contactPoint={basicContactPoint} />);
|
renderWithProvider(<ContactPoint contactPoint={basicContactPoint} />);
|
||||||
|
|
||||||
|
@ -224,16 +224,10 @@ const ContactPointsPageContents = () => {
|
|||||||
interface ContactPointsListProps {
|
interface ContactPointsListProps {
|
||||||
contactPoints: ContactPointWithMetadata[];
|
contactPoints: ContactPointWithMetadata[];
|
||||||
search?: string | null;
|
search?: string | null;
|
||||||
disabled?: boolean;
|
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactPointsList = ({
|
const ContactPointsList = ({ contactPoints, search, pageSize = DEFAULT_PAGE_SIZE }: ContactPointsListProps) => {
|
||||||
contactPoints,
|
|
||||||
disabled = false,
|
|
||||||
search,
|
|
||||||
pageSize = DEFAULT_PAGE_SIZE,
|
|
||||||
}: ContactPointsListProps) => {
|
|
||||||
const searchResults = useContactPointsSearch(contactPoints, search);
|
const searchResults = useContactPointsSearch(contactPoints, search);
|
||||||
const { page, pageItems, numberOfPages, onPageChange } = usePagination(searchResults, 1, pageSize);
|
const { page, pageItems, numberOfPages, onPageChange } = usePagination(searchResults, 1, pageSize);
|
||||||
|
|
||||||
@ -241,7 +235,7 @@ const ContactPointsList = ({
|
|||||||
<>
|
<>
|
||||||
{pageItems.map((contactPoint, index) => {
|
{pageItems.map((contactPoint, index) => {
|
||||||
const key = `${contactPoint.name}-${index}`;
|
const key = `${contactPoint.name}-${index}`;
|
||||||
return <ContactPoint key={key} contactPoint={contactPoint} disabled={disabled} />;
|
return <ContactPoint key={key} contactPoint={contactPoint} />;
|
||||||
})}
|
})}
|
||||||
<Pagination currentPage={page} numberOfPages={numberOfPages} onNavigate={onPageChange} hideWhenSinglePage />
|
<Pagination currentPage={page} numberOfPages={numberOfPages} onNavigate={onPageChange} hideWhenSinglePage />
|
||||||
</>
|
</>
|
||||||
|
@ -3,18 +3,12 @@
|
|||||||
* and (if available) it will also fetch the status from the Grafana Managed status endpoint
|
* and (if available) it will also fetch the status from the Grafana Managed status endpoint
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { produce } from 'immer';
|
import { merge, set } from 'lodash';
|
||||||
import { merge, remove, set } from 'lodash';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { alertingApi } from 'app/features/alerting/unified/api/alertingApi';
|
|
||||||
import { receiversApi } from 'app/features/alerting/unified/api/receiversK8sApi';
|
import { receiversApi } from 'app/features/alerting/unified/api/receiversK8sApi';
|
||||||
import { useOnCallIntegration } from 'app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration';
|
import { useOnCallIntegration } from 'app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration';
|
||||||
import {
|
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver } from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
||||||
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver,
|
|
||||||
generatedReceiversApi,
|
|
||||||
} from 'app/features/alerting/unified/openapi/receiversApi.gen';
|
|
||||||
import { updateAlertManagerConfigAction } from 'app/features/alerting/unified/state/actions';
|
|
||||||
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/types/hooks';
|
||||||
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
|
import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
||||||
@ -23,17 +17,18 @@ import {
|
|||||||
isK8sEntityProvisioned,
|
isK8sEntityProvisioned,
|
||||||
shouldUseK8sApi,
|
shouldUseK8sApi,
|
||||||
} from 'app/features/alerting/unified/utils/k8s/utils';
|
} from 'app/features/alerting/unified/utils/k8s/utils';
|
||||||
import { updateConfigWithReceiver } from 'app/features/alerting/unified/utils/receiver-form';
|
|
||||||
import {
|
import {
|
||||||
GrafanaManagedContactPoint,
|
GrafanaManagedContactPoint,
|
||||||
GrafanaManagedReceiverConfig,
|
GrafanaManagedReceiverConfig,
|
||||||
Receiver,
|
Receiver,
|
||||||
} from 'app/plugins/datasource/alertmanager/types';
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { useDispatch } from 'app/types';
|
|
||||||
|
|
||||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||||
import { onCallApi } from '../../api/onCallApi';
|
import { onCallApi } from '../../api/onCallApi';
|
||||||
|
import { useAsync } from '../../hooks/useAsync';
|
||||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||||
|
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
|
||||||
|
import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers';
|
||||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||||
|
|
||||||
import { enhanceContactPointsWithMetadata } from './utils';
|
import { enhanceContactPointsWithMetadata } from './utils';
|
||||||
@ -54,7 +49,6 @@ const {
|
|||||||
useGetContactPointsStatusQuery,
|
useGetContactPointsStatusQuery,
|
||||||
useGrafanaNotifiersQuery,
|
useGrafanaNotifiersQuery,
|
||||||
useLazyGetAlertmanagerConfigurationQuery,
|
useLazyGetAlertmanagerConfigurationQuery,
|
||||||
useUpdateAlertmanagerConfigurationMutation,
|
|
||||||
} = alertmanagerApi;
|
} = alertmanagerApi;
|
||||||
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
|
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
|
||||||
const {
|
const {
|
||||||
@ -311,34 +305,28 @@ export function useContactPointsWithStatus({
|
|||||||
return isGrafanaAlertmanager ? grafanaResponse : alertmanagerConfigResponse;
|
return isGrafanaAlertmanager ? grafanaResponse : alertmanagerConfigResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteContactPointArgs = { name: string; resourceVersion?: string };
|
||||||
export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) {
|
export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) {
|
||||||
const [fetchAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
|
|
||||||
const [updateAlertManager] = useUpdateAlertmanagerConfigurationMutation();
|
|
||||||
const [deleteReceiver] = useDeleteNamespacedReceiverMutation();
|
|
||||||
|
|
||||||
const useK8sApi = shouldUseK8sApi(alertmanager);
|
const useK8sApi = shouldUseK8sApi(alertmanager);
|
||||||
|
|
||||||
return async ({ name, resourceVersion }: { name: string; resourceVersion?: string }) => {
|
const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
|
||||||
if (useK8sApi) {
|
const [deleteReceiver] = useDeleteNamespacedReceiverMutation();
|
||||||
|
|
||||||
|
const deleteFromK8sAPI = useAsync(async ({ name, resourceVersion }: DeleteContactPointArgs) => {
|
||||||
const namespace = getK8sNamespace();
|
const namespace = getK8sNamespace();
|
||||||
return deleteReceiver({
|
await deleteReceiver({
|
||||||
name,
|
name,
|
||||||
namespace,
|
namespace,
|
||||||
ioK8SApimachineryPkgApisMetaV1DeleteOptions: { preconditions: { resourceVersion } },
|
ioK8SApimachineryPkgApisMetaV1DeleteOptions: { preconditions: { resourceVersion } },
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
}
|
|
||||||
const config = await fetchAlertmanagerConfig(alertmanager).unwrap();
|
|
||||||
|
|
||||||
const newConfig = produce(config, (draft) => {
|
|
||||||
remove(draft?.alertmanager_config?.receivers ?? [], (receiver) => receiver.name === name);
|
|
||||||
return draft;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return updateAlertManager({
|
const deleteFromAlertmanagerConfiguration = useAsync(async ({ name }: DeleteContactPointArgs) => {
|
||||||
selectedAlertmanager: alertmanager,
|
const action = deleteReceiverAction(name);
|
||||||
config: newConfig,
|
return produceNewAlertmanagerConfiguration(action);
|
||||||
}).unwrap();
|
});
|
||||||
};
|
|
||||||
|
return useK8sApi ? deleteFromK8sAPI : deleteFromAlertmanagerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -404,75 +392,69 @@ type ContactPointOperationArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type CreateContactPointArgs = ContactPointOperationArgs;
|
type CreateContactPointArgs = ContactPointOperationArgs;
|
||||||
type UpdateContactPointArgs = ContactPointOperationArgs & {
|
|
||||||
/** ID of existing contact point to update - used when updating via k8s API */
|
|
||||||
id?: string;
|
|
||||||
resourceVersion?: string;
|
|
||||||
/** Name of the existing contact point - used for checking uniqueness of name when not using k8s API*/
|
|
||||||
originalName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCreateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
|
export const useCreateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
|
||||||
const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
|
const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||||
const useK8sApi = shouldUseK8sApi(alertmanager);
|
const useK8sApi = shouldUseK8sApi(alertmanager);
|
||||||
|
|
||||||
const { createOnCallIntegrations } = useOnCallIntegration();
|
const { createOnCallIntegrations } = useOnCallIntegration();
|
||||||
const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
|
|
||||||
const [createGrafanaContactPoint] = useCreateNamespacedReceiverMutation();
|
const [createGrafanaContactPoint] = useCreateNamespacedReceiverMutation();
|
||||||
const dispatch = useDispatch();
|
const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
|
||||||
|
|
||||||
return async ({ contactPoint }: CreateContactPointArgs) => {
|
const updateK8sAPI = useAsync(async ({ contactPoint }: CreateContactPointArgs) => {
|
||||||
const receiverWithPotentialOnCall = isGrafanaAlertmanager
|
const contactPointWithMaybeOnCall = isGrafanaAlertmanager
|
||||||
? await createOnCallIntegrations(contactPoint)
|
? await createOnCallIntegrations(contactPoint)
|
||||||
: contactPoint;
|
: contactPoint;
|
||||||
|
|
||||||
if (useK8sApi) {
|
|
||||||
const namespace = getK8sNamespace();
|
const namespace = getK8sNamespace();
|
||||||
|
const contactPointToUse = grafanaContactPointToK8sReceiver(contactPointWithMaybeOnCall);
|
||||||
const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall);
|
|
||||||
|
|
||||||
return createGrafanaContactPoint({
|
return createGrafanaContactPoint({
|
||||||
namespace,
|
namespace,
|
||||||
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
|
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
}
|
|
||||||
|
|
||||||
const config = await getAlertmanagerConfig(alertmanager).unwrap();
|
|
||||||
const newConfig = updateConfigWithReceiver(config, receiverWithPotentialOnCall);
|
|
||||||
|
|
||||||
return await dispatch(
|
|
||||||
updateAlertManagerConfigAction({
|
|
||||||
newConfig: newConfig,
|
|
||||||
oldConfig: config,
|
|
||||||
alertManagerSourceName: alertmanager,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.then(() => {
|
|
||||||
dispatch(alertingApi.util.invalidateTags(['AlertmanagerConfiguration', 'ContactPoint', 'ContactPointsStatus']));
|
|
||||||
dispatch(generatedReceiversApi.util.invalidateTags(['Receiver']));
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
const updateAlertmanagerConfiguration = useAsync(async ({ contactPoint }: CreateContactPointArgs) => {
|
||||||
|
const contactPointWithMaybeOnCall = isGrafanaAlertmanager
|
||||||
|
? await createOnCallIntegrations(contactPoint)
|
||||||
|
: contactPoint;
|
||||||
|
|
||||||
|
const action = addReceiverAction(contactPointWithMaybeOnCall);
|
||||||
|
return produceNewAlertmanagerConfiguration(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
return useK8sApi ? updateK8sAPI : updateAlertmanagerConfiguration;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpdateContactPointArgsK8s = ContactPointOperationArgs & {
|
||||||
|
/** ID of existing contact point to update - used when updating via k8s API */
|
||||||
|
id: string;
|
||||||
|
resourceVersion?: string;
|
||||||
|
};
|
||||||
|
type UpdateContactPointArgsConfig = ContactPointOperationArgs & {
|
||||||
|
/** Name of the existing contact point - used for checking uniqueness of name when not using k8s API*/
|
||||||
|
originalName: string;
|
||||||
|
};
|
||||||
|
type UpdateContactpointArgs = UpdateContactPointArgsK8s | UpdateContactPointArgsConfig;
|
||||||
|
|
||||||
export const useUpdateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
|
export const useUpdateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
|
||||||
const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
|
const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME;
|
||||||
const useK8sApi = shouldUseK8sApi(alertmanager);
|
const useK8sApi = shouldUseK8sApi(alertmanager);
|
||||||
|
|
||||||
const { createOnCallIntegrations } = useOnCallIntegration();
|
const { createOnCallIntegrations } = useOnCallIntegration();
|
||||||
const [getAlertmanagerConfig] = useLazyGetAlertmanagerConfigurationQuery();
|
|
||||||
const [replaceGrafanaContactPoint] = useReplaceNamespacedReceiverMutation();
|
const [replaceGrafanaContactPoint] = useReplaceNamespacedReceiverMutation();
|
||||||
|
const [produceNewAlertmanagerConfiguration] = useProduceNewAlertmanagerConfiguration();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const updateContactPoint = useAsync(async (args: UpdateContactpointArgs) => {
|
||||||
|
if ('resourceVersion' in args && useK8sApi) {
|
||||||
|
const { contactPoint, id, resourceVersion } = args;
|
||||||
|
|
||||||
return async ({ contactPoint, id, originalName, resourceVersion }: UpdateContactPointArgs) => {
|
|
||||||
const receiverWithPotentialOnCall = isGrafanaAlertmanager
|
const receiverWithPotentialOnCall = isGrafanaAlertmanager
|
||||||
? await createOnCallIntegrations(contactPoint)
|
? await createOnCallIntegrations(contactPoint)
|
||||||
: contactPoint;
|
: contactPoint;
|
||||||
|
|
||||||
if (useK8sApi && id) {
|
|
||||||
const namespace = getK8sNamespace();
|
const namespace = getK8sNamespace();
|
||||||
|
|
||||||
const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall, id, resourceVersion);
|
const contactPointToUse = grafanaContactPointToK8sReceiver(receiverWithPotentialOnCall, id, resourceVersion);
|
||||||
|
|
||||||
return replaceGrafanaContactPoint({
|
return replaceGrafanaContactPoint({
|
||||||
@ -480,24 +462,18 @@ export const useUpdateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) =>
|
|||||||
namespace,
|
namespace,
|
||||||
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
|
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1Receiver: contactPointToUse,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
} else if ('originalName' in args) {
|
||||||
|
const { contactPoint, originalName } = args;
|
||||||
|
const receiverWithPotentialOnCall = isGrafanaAlertmanager
|
||||||
|
? await createOnCallIntegrations(contactPoint)
|
||||||
|
: contactPoint;
|
||||||
|
|
||||||
|
const action = updateReceiverAction({ name: originalName, receiver: receiverWithPotentialOnCall });
|
||||||
|
return produceNewAlertmanagerConfiguration(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getAlertmanagerConfig(alertmanager).unwrap();
|
|
||||||
const newConfig = updateConfigWithReceiver(config, receiverWithPotentialOnCall, originalName);
|
|
||||||
|
|
||||||
return dispatch(
|
|
||||||
updateAlertManagerConfigAction({
|
|
||||||
newConfig: newConfig,
|
|
||||||
oldConfig: config,
|
|
||||||
alertManagerSourceName: alertmanager,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.then(() => {
|
|
||||||
dispatch(alertingApi.util.invalidateTags(['AlertmanagerConfiguration', 'ContactPoint', 'ContactPointsStatus']));
|
|
||||||
dispatch(generatedReceiversApi.util.invalidateTags(['Receiver']));
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
return updateContactPoint;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useValidateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
|
export const useValidateContactPoint = ({ alertmanager }: BaseAlertmanagerArgs) => {
|
@ -9,7 +9,7 @@ import {
|
|||||||
} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
} from 'app/features/alerting/unified/components/contact-points/useContactPoints';
|
||||||
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
|
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
import { CloudChannelValues, ReceiverFormValues, CloudChannelMap } from '../../../types/receiver-form';
|
import { CloudChannelMap, CloudChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
||||||
import { cloudNotifierTypes } from '../../../utils/cloud-alertmanager-notifier-types';
|
import { cloudNotifierTypes } from '../../../utils/cloud-alertmanager-notifier-types';
|
||||||
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
import { isVanillaPrometheusAlertManagerDataSource } from '../../../utils/datasource';
|
||||||
import { cloudReceiverToFormValues, formValuesToCloudReceiver } from '../../../utils/receiver-form';
|
import { cloudReceiverToFormValues, formValuesToCloudReceiver } from '../../../utils/receiver-form';
|
||||||
@ -41,8 +41,8 @@ export const CloudReceiverForm = ({ contactPoint, alertManagerSourceName, readOn
|
|||||||
const { isLoading, data: config } = useGetAlertmanagerConfigurationQuery(alertManagerSourceName);
|
const { isLoading, data: config } = useGetAlertmanagerConfigurationQuery(alertManagerSourceName);
|
||||||
|
|
||||||
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||||
const createContactPoint = useCreateContactPoint({ alertmanager: alertManagerSourceName });
|
const [createContactPoint] = useCreateContactPoint({ alertmanager: alertManagerSourceName });
|
||||||
const updateContactPoint = useUpdateContactPoint({ alertmanager: alertManagerSourceName });
|
const [updateContactPoint] = useUpdateContactPoint({ alertmanager: alertManagerSourceName });
|
||||||
|
|
||||||
// transform receiver DTO to form values
|
// transform receiver DTO to form values
|
||||||
const [existingValue] = useMemo((): [ReceiverFormValues<CloudChannelValues> | undefined, CloudChannelMap] => {
|
const [existingValue] = useMemo((): [ReceiverFormValues<CloudChannelValues> | undefined, CloudChannelMap] => {
|
||||||
@ -54,11 +54,12 @@ export const CloudReceiverForm = ({ contactPoint, alertManagerSourceName, readOn
|
|||||||
|
|
||||||
const onSubmit = async (values: ReceiverFormValues<CloudChannelValues>) => {
|
const onSubmit = async (values: ReceiverFormValues<CloudChannelValues>) => {
|
||||||
const newReceiver = formValuesToCloudReceiver(values, defaultChannelValues);
|
const newReceiver = formValuesToCloudReceiver(values, defaultChannelValues);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (editMode) {
|
if (editMode && contactPoint) {
|
||||||
await updateContactPoint({ contactPoint: newReceiver, originalName: contactPoint!.name });
|
await updateContactPoint.execute({ contactPoint: newReceiver, originalName: contactPoint.name });
|
||||||
} else {
|
} else {
|
||||||
await createContactPoint({ contactPoint: newReceiver });
|
await createContactPoint.execute({ contactPoint: newReceiver });
|
||||||
}
|
}
|
||||||
locationService.push('/alerting/notifications');
|
locationService.push('/alerting/notifications');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,18 +1,21 @@
|
|||||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||||
import { render, waitFor, screen } from 'test/test-utils';
|
import { screen, waitFor } from 'test/test-utils';
|
||||||
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
|
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
|
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
|
||||||
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
|
|
||||||
import {
|
import {
|
||||||
setOnCallFeatures,
|
setOnCallFeatures,
|
||||||
setOnCallIntegrations,
|
setOnCallIntegrations,
|
||||||
} from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
|
} from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
|
||||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi';
|
import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi';
|
||||||
|
import { grantUserPermissions } from '../../../mocks';
|
||||||
|
import { captureRequests } from '../../../mocks/server/events';
|
||||||
|
import { renderWithProvider } from '../../contact-points/ContactPoints.test';
|
||||||
|
|
||||||
import { GrafanaReceiverForm } from './GrafanaReceiverForm';
|
import { GrafanaReceiverForm } from './GrafanaReceiverForm';
|
||||||
|
|
||||||
@ -35,6 +38,13 @@ const ui = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('GrafanaReceiverForm', () => {
|
describe('GrafanaReceiverForm', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
grantUserPermissions([
|
||||||
|
AccessControlAction.AlertingNotificationsRead,
|
||||||
|
AccessControlAction.AlertingNotificationsWrite,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
describe('alertingApiServer', () => {
|
describe('alertingApiServer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
config.featureToggles.alertingApiServer = true;
|
config.featureToggles.alertingApiServer = true;
|
||||||
@ -47,7 +57,7 @@ describe('GrafanaReceiverForm', () => {
|
|||||||
const capturedRequests = captureRequests(
|
const capturedRequests = captureRequests(
|
||||||
(req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST'
|
(req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST'
|
||||||
);
|
);
|
||||||
const { user } = render(<GrafanaReceiverForm />);
|
const { user } = renderWithProvider(<GrafanaReceiverForm />);
|
||||||
const { type, click } = user;
|
const { type, click } = user;
|
||||||
|
|
||||||
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
||||||
@ -84,7 +94,7 @@ describe('GrafanaReceiverForm', () => {
|
|||||||
it('OnCall contact point should be disabled if OnCall integration is not enabled', async () => {
|
it('OnCall contact point should be disabled if OnCall integration is not enabled', async () => {
|
||||||
disablePlugin(SupportedPlugin.OnCall);
|
disablePlugin(SupportedPlugin.OnCall);
|
||||||
|
|
||||||
render(<GrafanaReceiverForm />);
|
renderWithProvider(<GrafanaReceiverForm />);
|
||||||
|
|
||||||
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
||||||
|
|
||||||
@ -104,7 +114,7 @@ describe('GrafanaReceiverForm', () => {
|
|||||||
{ display_name: 'apac-oncall', value: 'apac-oncall', integration_url: 'https://apac.oncall.example.com' },
|
{ display_name: 'apac-oncall', value: 'apac-oncall', integration_url: 'https://apac.oncall.example.com' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { user } = render(<GrafanaReceiverForm />);
|
const { user } = renderWithProvider(<GrafanaReceiverForm />);
|
||||||
|
|
||||||
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
||||||
|
|
||||||
@ -156,7 +166,7 @@ describe('GrafanaReceiverForm', () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
render(<GrafanaReceiverForm contactPoint={amConfig.alertmanager_config.receivers![0]} />);
|
renderWithProvider(<GrafanaReceiverForm contactPoint={amConfig.alertmanager_config.receivers![0]} />);
|
||||||
|
|
||||||
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import { useDispatch } from 'app/types';
|
|||||||
import { alertmanagerApi } from '../../../api/alertmanagerApi';
|
import { alertmanagerApi } from '../../../api/alertmanagerApi';
|
||||||
import { testReceiversAction } from '../../../state/actions';
|
import { testReceiversAction } from '../../../state/actions';
|
||||||
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
import { GrafanaChannelValues, ReceiverFormValues } from '../../../types/receiver-form';
|
||||||
|
import { shouldUseK8sApi } from '../../../utils/k8s/utils';
|
||||||
import {
|
import {
|
||||||
formChannelValuesToGrafanaChannelConfig,
|
formChannelValuesToGrafanaChannelConfig,
|
||||||
formValuesToGrafanaReceiver,
|
formValuesToGrafanaReceiver,
|
||||||
@ -52,8 +53,13 @@ const { useGrafanaNotifiersQuery } = alertmanagerApi;
|
|||||||
|
|
||||||
export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }: Props) => {
|
export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }: Props) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const createContactPoint = useCreateContactPoint({ alertmanager: GRAFANA_RULES_SOURCE_NAME });
|
const useK8sAPI = shouldUseK8sApi(GRAFANA_RULES_SOURCE_NAME);
|
||||||
const updateContactPoint = useUpdateContactPoint({ alertmanager: GRAFANA_RULES_SOURCE_NAME });
|
const [createContactPoint] = useCreateContactPoint({
|
||||||
|
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
});
|
||||||
|
const [updateContactPoint] = useUpdateContactPoint({
|
||||||
|
alertmanager: GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
onCallNotifierMeta,
|
onCallNotifierMeta,
|
||||||
@ -82,16 +88,23 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
|||||||
|
|
||||||
const onSubmit = async (values: ReceiverFormValues<GrafanaChannelValues>) => {
|
const onSubmit = async (values: ReceiverFormValues<GrafanaChannelValues>) => {
|
||||||
const newReceiver = formValuesToGrafanaReceiver(values, id2original, defaultChannelValues, grafanaNotifiers);
|
const newReceiver = formValuesToGrafanaReceiver(values, id2original, defaultChannelValues, grafanaNotifiers);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
await updateContactPoint({
|
if (useK8sAPI && contactPoint && contactPoint.id) {
|
||||||
|
await updateContactPoint.execute({
|
||||||
contactPoint: newReceiver,
|
contactPoint: newReceiver,
|
||||||
id: contactPoint!.id,
|
id: contactPoint.id,
|
||||||
resourceVersion: contactPoint?.metadata?.resourceVersion,
|
resourceVersion: contactPoint?.metadata?.resourceVersion,
|
||||||
originalName: contactPoint?.name,
|
|
||||||
});
|
});
|
||||||
|
} else if (contactPoint) {
|
||||||
|
await updateContactPoint.execute({
|
||||||
|
contactPoint: newReceiver,
|
||||||
|
originalName: contactPoint.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await createContactPoint({ contactPoint: newReceiver });
|
await createContactPoint.execute({ contactPoint: newReceiver });
|
||||||
}
|
}
|
||||||
locationService.push('/alerting/notifications');
|
locationService.push('/alerting/notifications');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
interface RequestState {
|
|
||||||
error?: unknown;
|
|
||||||
|
|
||||||
isUninitialized: boolean;
|
|
||||||
isSuccess: boolean;
|
|
||||||
isError: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODO what to do with the other props that we get from RTKQ's state such as originalArgs, etc?
|
|
||||||
export function mergeRequestStates(...states: RequestState[]): RequestState {
|
|
||||||
return {
|
|
||||||
error: states.find((s) => s.error),
|
|
||||||
isUninitialized: states.every((s) => s.isUninitialized),
|
|
||||||
isSuccess: states.every((s) => s.isSuccess),
|
|
||||||
isError: states.some((s) => s.isError),
|
|
||||||
isLoading: states.some((s) => s.isLoading),
|
|
||||||
};
|
|
||||||
}
|
|
@ -5,6 +5,7 @@ import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/ty
|
|||||||
|
|
||||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||||
import { muteTimingsReducer } from '../reducers/alertmanager/muteTimings';
|
import { muteTimingsReducer } from '../reducers/alertmanager/muteTimings';
|
||||||
|
import { receiversReducer } from '../reducers/alertmanager/receivers';
|
||||||
import { useAlertmanager } from '../state/AlertmanagerContext';
|
import { useAlertmanager } from '../state/AlertmanagerContext';
|
||||||
|
|
||||||
import { mergeRequestStates } from './mergeRequestStates';
|
import { mergeRequestStates } from './mergeRequestStates';
|
||||||
@ -25,7 +26,7 @@ export const initialAlertmanagerConfiguration: AlertManagerCortexConfig = {
|
|||||||
template_files: {},
|
template_files: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const configurationReducer = reduceReducers(initialAlertmanagerConfiguration, muteTimingsReducer);
|
const configurationReducer = reduceReducers(initialAlertmanagerConfiguration, muteTimingsReducer, receiversReducer);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook will make sure we are always applying actions that mutate the Alertmanager configuration
|
* This hook will make sure we are always applying actions that mutate the Alertmanager configuration
|
||||||
|
@ -129,7 +129,7 @@ class GrafanaReceiverConfigBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlertmanagerReceiverBuilder {
|
export class AlertmanagerReceiverBuilder {
|
||||||
private receiver: AlertmanagerReceiver = { name: '', email_configs: [], grafana_managed_receiver_configs: [] };
|
private receiver: AlertmanagerReceiver = { name: '', email_configs: [], grafana_managed_receiver_configs: [] };
|
||||||
|
|
||||||
withName(name: string): AlertmanagerReceiverBuilder {
|
withName(name: string): AlertmanagerReceiverBuilder {
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`receivers adding receivers should be able to add a new Alertmanager receiver 1`] = `
|
||||||
|
{
|
||||||
|
"alertmanager_config": {
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"email_configs": [
|
||||||
|
{
|
||||||
|
"to": "address@domain.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"grafana_managed_receiver_configs": [],
|
||||||
|
"name": "new contact point",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"template_files": {},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`receivers adding receivers should be able to add a new Grafana Alertmanager receiver 1`] = `
|
||||||
|
{
|
||||||
|
"alertmanager_config": {
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"email_configs": [],
|
||||||
|
"grafana_managed_receiver_configs": [
|
||||||
|
{
|
||||||
|
"disableResolveMessage": false,
|
||||||
|
"name": "emea-oncall",
|
||||||
|
"settings": {
|
||||||
|
"url": "https://oncall.example.com",
|
||||||
|
},
|
||||||
|
"type": "oncall",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "another contact point",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"template_files": {},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`receivers should delete a receiver 1`] = `
|
||||||
|
{
|
||||||
|
"alertmanager_config": {
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"name": "another receiver",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"template_files": {},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`receivers updating receivers should allow renaming a receiver and update routes 1`] = `
|
||||||
|
{
|
||||||
|
"alertmanager_config": {
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"email_configs": [],
|
||||||
|
"grafana_managed_receiver_configs": [],
|
||||||
|
"name": "receiver 2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"receiver": "receiver 2",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"receiver": "receiver 2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"template_files": {},
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`receivers updating receivers should allow updating an existing receiver 1`] = `
|
||||||
|
{
|
||||||
|
"alertmanager_config": {
|
||||||
|
"receivers": [
|
||||||
|
{
|
||||||
|
"email_configs": [
|
||||||
|
{
|
||||||
|
"to": "address+1@domain.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"grafana_managed_receiver_configs": [],
|
||||||
|
"name": "existing receiver",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"template_files": {},
|
||||||
|
}
|
||||||
|
`;
|
@ -1,5 +1,3 @@
|
|||||||
import { UnknownAction } from 'redux';
|
|
||||||
|
|
||||||
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
import { addMuteTimingAction, deleteMuteTimingAction, muteTimingsReducer, updateMuteTimingAction } from './muteTimings';
|
import { addMuteTimingAction, deleteMuteTimingAction, muteTimingsReducer, updateMuteTimingAction } from './muteTimings';
|
||||||
@ -54,14 +52,6 @@ describe('mute timings', () => {
|
|||||||
const updateMuteTiming = updateMuteTimingAction({ originalName: muteTimingName, interval: newMuteTiming });
|
const updateMuteTiming = updateMuteTimingAction({ originalName: muteTimingName, interval: newMuteTiming });
|
||||||
expect(muteTimingsReducer(initialConfig, updateMuteTiming)).toMatchSnapshot();
|
expect(muteTimingsReducer(initialConfig, updateMuteTiming)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw for unknown action', () => {
|
|
||||||
const action: UnknownAction = { type: 'unknown' };
|
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
muteTimingsReducer(initialConfig, action);
|
|
||||||
}).toThrow('unknown');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function mockTimeInterval(overrides: Partial<MuteTimeInterval> = {}): MuteTimeInterval {
|
function mockTimeInterval(overrides: Partial<MuteTimeInterval> = {}): MuteTimeInterval {
|
||||||
|
@ -68,8 +68,5 @@ export const muteTimingsReducer = createReducer(initialState, (builder) => {
|
|||||||
|
|
||||||
// remove the mute timing from all routes
|
// remove the mute timing from all routes
|
||||||
alertmanager_config.route = removeTimeIntervalFromRoute(name, alertmanager_config.route ?? {});
|
alertmanager_config.route = removeTimeIntervalFromRoute(name, alertmanager_config.route ?? {});
|
||||||
})
|
|
||||||
.addDefaultCase((_state, action) => {
|
|
||||||
throw new Error(`Unknown action for mute timing reducer: ${action.type}`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,138 @@
|
|||||||
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
|
import { AlertmanagerReceiverBuilder } from '../../mockApi';
|
||||||
|
|
||||||
|
import { addReceiverAction, deleteReceiverAction, receiversReducer, updateReceiverAction } from './receivers';
|
||||||
|
|
||||||
|
describe('receivers', () => {
|
||||||
|
const initialConfig: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should delete a receiver', () => {
|
||||||
|
const config: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'my receiver' }, { name: 'another receiver' }],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = deleteReceiverAction('my receiver');
|
||||||
|
expect(receiversReducer(config, action)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('adding receivers', () => {
|
||||||
|
it('should be able to add a new Alertmanager receiver', () => {
|
||||||
|
const newReceiver = new AlertmanagerReceiverBuilder()
|
||||||
|
.withName('new contact point')
|
||||||
|
.addEmailConfig((b) => b.withTo('address@domain.com'))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const action = addReceiverAction(newReceiver);
|
||||||
|
expect(receiversReducer(initialConfig, action)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to add a new Grafana Alertmanager receiver', () => {
|
||||||
|
const newReceiver = new AlertmanagerReceiverBuilder()
|
||||||
|
.withName('another contact point')
|
||||||
|
.addGrafanaReceiverConfig((receiver) =>
|
||||||
|
receiver.withType('oncall').withName('emea-oncall').addSetting('url', 'https://oncall.example.com')
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const action = addReceiverAction(newReceiver);
|
||||||
|
expect(receiversReducer(initialConfig, action)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if we add a receiver with duplicate name', () => {
|
||||||
|
const config: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'my receiver' }],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newReceiver = new AlertmanagerReceiverBuilder().withName('my receiver').build();
|
||||||
|
const action = addReceiverAction(newReceiver);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
receiversReducer(config, action);
|
||||||
|
}).toThrow(/duplicate receiver/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updating receivers', () => {
|
||||||
|
it('should throw if updating a receiver that does not exist', () => {
|
||||||
|
const config: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'my receiver' }],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedReceiver = new AlertmanagerReceiverBuilder().withName('my receiver').build();
|
||||||
|
const action = updateReceiverAction({ name: 'does not exist', receiver: updatedReceiver });
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
receiversReducer(config, action);
|
||||||
|
}).toThrow(/expected receiver .+ to exist/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if renaming a receiver to an existing name', () => {
|
||||||
|
const config: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'receiver 1' }, { name: 'receiver 2' }],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedReceiver = new AlertmanagerReceiverBuilder().withName('receiver 1').build();
|
||||||
|
const action = updateReceiverAction({ name: 'receiver 2', receiver: updatedReceiver });
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
receiversReducer(config, action);
|
||||||
|
}).toThrow(/duplicate receiver name/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow renaming a receiver and update routes', () => {
|
||||||
|
const config: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [{ name: 'receiver 1' }],
|
||||||
|
route: {
|
||||||
|
receiver: 'receiver 1',
|
||||||
|
routes: [{ receiver: 'receiver 1' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedReceiver = new AlertmanagerReceiverBuilder().withName('receiver 2').build();
|
||||||
|
const action = updateReceiverAction({ name: 'receiver 1', receiver: updatedReceiver });
|
||||||
|
|
||||||
|
expect(receiversReducer(config, action)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow updating an existing receiver', () => {
|
||||||
|
const existingReceiver = new AlertmanagerReceiverBuilder()
|
||||||
|
.withName('existing receiver')
|
||||||
|
.addEmailConfig((build) => build.withTo('address@domain.com'))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const config: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {
|
||||||
|
receivers: [existingReceiver],
|
||||||
|
},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedReceiver = new AlertmanagerReceiverBuilder()
|
||||||
|
.withName('existing receiver')
|
||||||
|
.addEmailConfig((build) => build.withTo('address+1@domain.com'))
|
||||||
|
.build();
|
||||||
|
const action = updateReceiverAction({ name: 'existing receiver', receiver: updatedReceiver });
|
||||||
|
|
||||||
|
expect(receiversReducer(config, action)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,74 @@
|
|||||||
|
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { remove } from 'lodash';
|
||||||
|
|
||||||
|
import { AlertManagerCortexConfig, Receiver } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
|
||||||
|
import { renameReceiverInRoute } from '../../utils/notification-policies';
|
||||||
|
|
||||||
|
export const addReceiverAction = createAction<Receiver>('receiver/add');
|
||||||
|
export const updateReceiverAction = createAction<{ name: string; receiver: Receiver }>('receiver/update');
|
||||||
|
export const deleteReceiverAction = createAction<string>('receiver/delete');
|
||||||
|
|
||||||
|
const initialState: AlertManagerCortexConfig = {
|
||||||
|
alertmanager_config: {},
|
||||||
|
template_files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This reducer will manage action related to receiver (Contact points) and make sure all operations on the alertmanager
|
||||||
|
* configuration happen immutably and only mutate what they need.
|
||||||
|
*/
|
||||||
|
export const receiversReducer = createReducer(initialState, (builder) => {
|
||||||
|
builder
|
||||||
|
// add a new receiver
|
||||||
|
.addCase(addReceiverAction, (draft, { payload: newReceiver }) => {
|
||||||
|
// ensure the receivers are always an array
|
||||||
|
const currentReceivers = (draft.alertmanager_config.receivers = draft.alertmanager_config.receivers ?? []);
|
||||||
|
|
||||||
|
// check if the name doesn't already exist
|
||||||
|
const nameExists = currentReceivers.some((receiver) => receiver.name === newReceiver.name);
|
||||||
|
if (nameExists) {
|
||||||
|
throw new Error(`Duplicate receiver name ${newReceiver.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the receiver
|
||||||
|
currentReceivers.push(newReceiver);
|
||||||
|
})
|
||||||
|
// upate an existing receiver
|
||||||
|
.addCase(updateReceiverAction, (draft, { payload }) => {
|
||||||
|
const { name, receiver } = payload;
|
||||||
|
const renaming = name !== receiver.name;
|
||||||
|
|
||||||
|
const receivers = draft.alertmanager_config.receivers ?? [];
|
||||||
|
|
||||||
|
const targetIndex = receivers.findIndex((receiver) => receiver.name === name);
|
||||||
|
const targetExists = targetIndex > -1;
|
||||||
|
|
||||||
|
// check if the receiver we want to update exists
|
||||||
|
if (!targetExists) {
|
||||||
|
throw new Error(`Expected receiver ${name} to exist, but did not find it in the config`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the new name doesn't already exist
|
||||||
|
if (renaming) {
|
||||||
|
const nameExists = receivers.some((oldReceiver) => oldReceiver.name === receiver.name);
|
||||||
|
if (nameExists) {
|
||||||
|
throw new Error(`Duplicate receiver name ${receiver.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overwrite the receiver with the new one
|
||||||
|
receivers[targetIndex] = receiver;
|
||||||
|
|
||||||
|
// check if we need to update routes if the contact point was renamed
|
||||||
|
const routeTree = draft.alertmanager_config.route;
|
||||||
|
|
||||||
|
if (routeTree && renaming) {
|
||||||
|
draft.alertmanager_config.route = renameReceiverInRoute(routeTree, name, receiver.name);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// delete a receiver from the alertmanager configuration
|
||||||
|
.addCase(deleteReceiverAction, (draft, { payload: name }) => {
|
||||||
|
remove(draft.alertmanager_config.receivers ?? [], (receiver) => receiver.name === name);
|
||||||
|
});
|
||||||
|
});
|
@ -342,36 +342,6 @@ export const updateAlertManagerConfigAction = createAsyncThunk<void, UpdateAlert
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteReceiverAction = (receiverName: string, alertManagerSourceName: string): ThunkResult<void> => {
|
|
||||||
return async (dispatch) => {
|
|
||||||
const config = await dispatch(
|
|
||||||
alertmanagerApi.endpoints.getAlertmanagerConfiguration.initiate(alertManagerSourceName)
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
throw new Error(`Config for ${alertManagerSourceName} not found`);
|
|
||||||
}
|
|
||||||
if (!config.alertmanager_config.receivers?.find((receiver) => receiver.name === receiverName)) {
|
|
||||||
throw new Error(`Cannot delete receiver ${receiverName}: not found in config.`);
|
|
||||||
}
|
|
||||||
const newConfig: AlertManagerCortexConfig = {
|
|
||||||
...config,
|
|
||||||
alertmanager_config: {
|
|
||||||
...config.alertmanager_config,
|
|
||||||
receivers: config.alertmanager_config.receivers.filter((receiver) => receiver.name !== receiverName),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return dispatch(
|
|
||||||
updateAlertManagerConfigAction({
|
|
||||||
newConfig,
|
|
||||||
oldConfig: config,
|
|
||||||
alertManagerSourceName,
|
|
||||||
successMessage: 'Contact point deleted.',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchFolderAction = createAsyncThunk(
|
export const fetchFolderAction = createAsyncThunk(
|
||||||
'unifiedalerting/fetchFolder',
|
'unifiedalerting/fetchFolder',
|
||||||
(uid: string): Promise<FolderDTO> => withSerializedError(backendSrv.getFolderByUid(uid, { withAccessControl: true }))
|
(uid: string): Promise<FolderDTO> => withSerializedError(backendSrv.getFolderByUid(uid, { withAccessControl: true }))
|
||||||
|
@ -281,4 +281,27 @@ function isLabelMatch(matcher: ObjectMatcher, label: Label): boolean {
|
|||||||
return matchFunction(labelValue, matcherValue);
|
return matchFunction(labelValue, matcherValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { findMatchingAlertGroups, findMatchingRoutes, getInheritedProperties, isLabelMatchInSet };
|
// recursive function to rename receivers in all routes (notification policies)
|
||||||
|
function renameReceiverInRoute(route: Route, oldName: string, newName: string) {
|
||||||
|
const updated: Route = {
|
||||||
|
...route,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updated.receiver === oldName) {
|
||||||
|
updated.receiver = newName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.routes) {
|
||||||
|
updated.routes = updated.routes.map((route) => renameReceiverInRoute(route, oldName, newName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
findMatchingAlertGroups,
|
||||||
|
findMatchingRoutes,
|
||||||
|
getInheritedProperties,
|
||||||
|
isLabelMatchInSet,
|
||||||
|
renameReceiverInRoute,
|
||||||
|
};
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { get, has, isArray, isNil, omit, omitBy, reduce } from 'lodash';
|
import { get, has, isArray, isNil, omit, omitBy, reduce } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlertManagerCortexConfig,
|
|
||||||
AlertmanagerReceiver,
|
AlertmanagerReceiver,
|
||||||
GrafanaManagedContactPoint,
|
GrafanaManagedContactPoint,
|
||||||
GrafanaManagedReceiverConfig,
|
GrafanaManagedReceiverConfig,
|
||||||
Receiver,
|
Receiver,
|
||||||
Route,
|
|
||||||
} from 'app/plugins/datasource/alertmanager/types';
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { CloudNotifierType, NotificationChannelOption, NotifierDTO, NotifierType } from 'app/types';
|
import { CloudNotifierType, NotificationChannelOption, NotifierDTO, NotifierType } from 'app/types';
|
||||||
|
|
||||||
@ -121,63 +119,6 @@ export function formValuesToCloudReceiver(
|
|||||||
return recv;
|
return recv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// will add new receiver, or replace exisitng one
|
|
||||||
export function updateConfigWithReceiver(
|
|
||||||
config: AlertManagerCortexConfig,
|
|
||||||
receiver: Receiver,
|
|
||||||
existingReceiverName?: string
|
|
||||||
): AlertManagerCortexConfig {
|
|
||||||
const existingReceivers = config.alertmanager_config.receivers ?? [];
|
|
||||||
|
|
||||||
const receiverWasRenamed = existingReceiverName && receiver.name !== existingReceiverName;
|
|
||||||
|
|
||||||
// sanity check that name is not duplicated
|
|
||||||
if (!existingReceiverName && !!existingReceivers.find(({ name }) => name === receiver.name)) {
|
|
||||||
throw new Error(`Duplicate receiver name ${receiver.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanity check that existing receiver exists
|
|
||||||
if (existingReceiverName && !existingReceivers.find(({ name }) => name === existingReceiverName)) {
|
|
||||||
throw new Error(`Expected receiver ${existingReceiverName} to exist, but did not find it in the config`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated: AlertManagerCortexConfig = {
|
|
||||||
...config,
|
|
||||||
alertmanager_config: {
|
|
||||||
...config.alertmanager_config,
|
|
||||||
receivers: existingReceiverName
|
|
||||||
? existingReceivers.map((existingReceiver) =>
|
|
||||||
existingReceiver.name === existingReceiverName ? receiver : existingReceiver
|
|
||||||
)
|
|
||||||
: [...existingReceivers, receiver],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// if receiver was renamed, rename it in routes as well
|
|
||||||
if (updated.alertmanager_config.route && receiverWasRenamed) {
|
|
||||||
updated.alertmanager_config.route = renameReceiverInRoute(
|
|
||||||
updated.alertmanager_config.route,
|
|
||||||
existingReceiverName,
|
|
||||||
receiver.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renameReceiverInRoute(route: Route, oldName: string, newName: string) {
|
|
||||||
const updated: Route = {
|
|
||||||
...route,
|
|
||||||
};
|
|
||||||
if (updated.receiver === oldName) {
|
|
||||||
updated.receiver = newName;
|
|
||||||
}
|
|
||||||
if (updated.routes) {
|
|
||||||
updated.routes = updated.routes.map((route) => renameReceiverInRoute(route, oldName, newName));
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cloudChannelConfigToFormChannelValues(
|
function cloudChannelConfigToFormChannelValues(
|
||||||
id: string,
|
id: string,
|
||||||
type: CloudNotifierType,
|
type: CloudNotifierType,
|
||||||
|
Loading…
Reference in New Issue
Block a user