Alerting: Remove disable logic for name field on time intervals when using k8s API (#91885)

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Tom Ratcliffe 2024-08-19 14:53:12 +01:00 committed by GitHub
parent 43dba8c3f9
commit e2a1d59a96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 57 additions and 48 deletions

View File

@ -13,8 +13,8 @@ import {
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events'; import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
import { MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID } from 'app/features/alerting/unified/mocks/server/handlers/datasources'; import { MOCK_DATASOURCE_EXTERNAL_VANILLA_ALERTMANAGER_UID } from 'app/features/alerting/unified/mocks/server/handlers/datasources';
import { import {
TIME_INTERVAL_UID_HAPPY_PATH, TIME_INTERVAL_NAME_HAPPY_PATH,
TIME_INTERVAL_UID_FILE_PROVISIONED, TIME_INTERVAL_NAME_FILE_PROVISIONED,
} from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources'; import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types'; import { AlertManagerCortexConfig, MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
@ -185,7 +185,7 @@ const fillOutForm = async ({
const saveMuteTiming = async () => { const saveMuteTiming = async () => {
const user = userEvent.setup(); const user = userEvent.setup();
await user.click(screen.getByText(/save mute timing/i)); await user.click(await screen.findByText(/save mute timing/i));
}; };
setupMswServer(); setupMswServer();
@ -365,7 +365,7 @@ describe('Mute timings', () => {
}); });
it('allows creation of new mute timings', async () => { it('allows creation of new mute timings', async () => {
await renderMuteTimings({ renderMuteTimings({
pathname: '/alerting/routes/mute-timing/new', pathname: '/alerting/routes/mute-timing/new',
}); });
@ -378,7 +378,7 @@ describe('Mute timings', () => {
it('shows error when mute timing does not exist', async () => { it('shows error when mute timing does not exist', async () => {
renderMuteTimings({ renderMuteTimings({
pathname: '/alerting/routes/mute-timing/edit', pathname: '/alerting/routes/mute-timing/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_UID_HAPPY_PATH + '_force_breakage'}`, search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`,
}); });
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument(); expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
@ -387,12 +387,9 @@ describe('Mute timings', () => {
it('loads edit form correctly and allows saving', async () => { it('loads edit form correctly and allows saving', async () => {
renderMuteTimings({ renderMuteTimings({
pathname: '/alerting/routes/mute-timing/edit', pathname: '/alerting/routes/mute-timing/edit',
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_UID_HAPPY_PATH}`, search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH}`,
}); });
// For now, we expect the name field to be disabled editing via the k8s API
expect(await ui.nameField.find()).toBeDisabled();
await saveMuteTiming(); await saveMuteTiming();
await expectedToHaveRedirectedToRoutesRoute(); await expectedToHaveRedirectedToRoutesRoute();
}); });
@ -400,7 +397,7 @@ describe('Mute timings', () => {
it('loads view form for provisioned interval', async () => { it('loads view form for provisioned interval', async () => {
renderMuteTimings({ renderMuteTimings({
pathname: '/alerting/routes/mute-timing/edit', pathname: '/alerting/routes/mute-timing/edit',
search: `?muteName=${TIME_INTERVAL_UID_FILE_PROVISIONED}`, search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`,
}); });
expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument(); expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument();

View File

@ -30,7 +30,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
const isGrafanaDataSource = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME; const isGrafanaDataSource = alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME;
const viewOrEditHref = makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, { const viewOrEditHref = makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, {
muteName: muteTiming?.metadata?.name || muteTiming.name, muteName: muteTiming.id,
}); });
const viewOrEditButton = ( const viewOrEditButton = (

View File

@ -11,7 +11,6 @@ import {
useUpdateMuteTiming, useUpdateMuteTiming,
useValidateMuteTiming, useValidateMuteTiming,
} from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils';
import { useAlertmanager } from '../../state/AlertmanagerContext'; import { useAlertmanager } from '../../state/AlertmanagerContext';
import { MuteTimingFields } from '../../types/mute-timing-form'; import { MuteTimingFields } from '../../types/mute-timing-form';
@ -65,13 +64,6 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
const [updateTimeInterval] = useUpdateMuteTiming(hookArgs); const [updateTimeInterval] = useUpdateMuteTiming(hookArgs);
const validateMuteTiming = useValidateMuteTiming(hookArgs); const validateMuteTiming = useValidateMuteTiming(hookArgs);
/**
* The k8s API approach does not support renaming an entity at this time,
* as it requires renaming all other references of this entity.
*
* For now, the cleanest solution is to disabled renaming the field in this scenario
*/
const disableNameField = editMode && shouldUseK8sApi(selectedAlertmanager!);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const defaultValues = useDefaultValues(muteTiming); const defaultValues = useDefaultValues(muteTiming);
@ -115,7 +107,6 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
description="A unique name for the mute timing" description="A unique name for the mute timing"
invalid={!!formApi.formState.errors?.name} invalid={!!formApi.formState.errors?.name}
error={formApi.formState.errors.name?.message} error={formApi.formState.errors.name?.message}
disabled={disableNameField}
> >
<Input <Input
{...formApi.register('name', { {...formApi.register('name', {

View File

@ -5,7 +5,7 @@ import { timeIntervalsApi } from 'app/features/alerting/unified/api/timeInterval
import { mergeTimeIntervals } from 'app/features/alerting/unified/components/mute-timings/util'; import { mergeTimeIntervals } from 'app/features/alerting/unified/components/mute-timings/util';
import { import {
ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval, ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval,
ReadNamespacedTimeIntervalApiResponse, IoK8SApimachineryPkgApisMetaV1ObjectMeta,
} from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks'; import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
@ -24,7 +24,6 @@ const { useLazyGetAlertmanagerConfigurationQuery } = alertmanagerApi;
const { const {
useLazyListNamespacedTimeIntervalQuery, useLazyListNamespacedTimeIntervalQuery,
useCreateNamespacedTimeIntervalMutation, useCreateNamespacedTimeIntervalMutation,
useLazyReadNamespacedTimeIntervalQuery,
useReplaceNamespacedTimeIntervalMutation, useReplaceNamespacedTimeIntervalMutation,
useDeleteNamespacedTimeIntervalMutation, useDeleteNamespacedTimeIntervalMutation,
} = timeIntervalsApi; } = timeIntervalsApi;
@ -35,7 +34,7 @@ const {
* */ * */
export type MuteTiming = MuteTimeInterval & { export type MuteTiming = MuteTimeInterval & {
id: string; id: string;
metadata?: ReadNamespacedTimeIntervalApiResponse['metadata']; metadata?: IoK8SApimachineryPkgApisMetaV1ObjectMeta;
}; };
/** Alias for generated kuberenetes Alerting API Server type */ /** Alias for generated kuberenetes Alerting API Server type */
@ -156,14 +155,18 @@ export const useCreateMuteTiming = ({ alertmanager }: BaseAlertmanagerArgs) => {
export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertmanagerArgs & { name: string }) => { export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertmanagerArgs & { name: string }) => {
const useK8sApi = shouldUseK8sApi(alertmanager); const useK8sApi = shouldUseK8sApi(alertmanager);
const [getGrafanaTimeInterval, k8sResponse] = useLazyReadNamespacedTimeIntervalQuery({ const [getGrafanaTimeInterval, k8sResponse] = useLazyListNamespacedTimeIntervalQuery({
selectFromResult: ({ data, ...rest }) => { selectFromResult: ({ data, ...rest }) => {
if (!data) { if (!data) {
return { data, ...rest }; return { data, ...rest };
} }
if (data.items.length === 0) {
return { ...rest, data: undefined, isError: true };
}
return { return {
data: parseK8sTimeInterval(data), data: parseK8sTimeInterval(data.items[0]),
...rest, ...rest,
}; };
}, },
@ -192,7 +195,7 @@ export const useGetMuteTiming = ({ alertmanager, name: nameToFind }: BaseAlertma
useEffect(() => { useEffect(() => {
if (useK8sApi) { if (useK8sApi) {
const namespace = getK8sNamespace(); const namespace = getK8sNamespace();
getGrafanaTimeInterval({ namespace, name: nameToFind }, true); getGrafanaTimeInterval({ namespace, fieldSelector: `spec.name=${nameToFind}` }, true);
} else { } else {
getAlertmanagerTimeInterval(alertmanager, true); getAlertmanagerTimeInterval(alertmanager, true);
} }

View File

@ -1,13 +1,18 @@
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { filterBySelector } from 'app/features/alerting/unified/mocks/server/handlers/k8s/utils';
import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse } from 'app/features/alerting/unified/mocks/server/utils';
import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen'; import { ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval } from 'app/features/alerting/unified/openapi/timeIntervalsApi.gen';
import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants'; import { PROVENANCE_ANNOTATION, PROVENANCE_NONE } from 'app/features/alerting/unified/utils/k8s/constants';
/** UID of a time interval that we expect to follow all happy paths within tests/mocks */ /** UID of a time interval that we expect to follow all happy paths within tests/mocks */
export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786'; export const TIME_INTERVAL_UID_HAPPY_PATH = 'f4eae7a4895fa786';
/** Display name of a time interval that we expect to follow all happy paths within tests/mocks */
export const TIME_INTERVAL_NAME_HAPPY_PATH = 'Some interval';
/** UID of a (file) provisioned time interval */ /** UID of a (file) provisioned time interval */
export const TIME_INTERVAL_UID_FILE_PROVISIONED = 'd7b8515fc39e90f7'; export const TIME_INTERVAL_UID_FILE_PROVISIONED = 'd7b8515fc39e90f7';
export const TIME_INTERVAL_NAME_FILE_PROVISIONED = 'A provisioned interval';
const allTimeIntervals = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval>( const allTimeIntervals = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval>(
'TimeIntervalList', 'TimeIntervalList',
@ -22,7 +27,7 @@ const allTimeIntervals = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNo
namespace: 'default', namespace: 'default',
resourceVersion: 'e0270bfced786660', resourceVersion: 'e0270bfced786660',
}, },
spec: { name: 'Some interval', time_intervals: [] }, spec: { name: TIME_INTERVAL_NAME_HAPPY_PATH, time_intervals: [] },
}, },
{ {
metadata: { metadata: {
@ -34,7 +39,7 @@ const allTimeIntervals = getK8sResponse<ComGithubGrafanaGrafanaPkgApisAlertingNo
namespace: 'default', namespace: 'default',
resourceVersion: 'a76d2fcc6731aa0c', resourceVersion: 'a76d2fcc6731aa0c',
}, },
spec: { name: 'A provisioned interval', time_intervals: [] }, spec: { name: TIME_INTERVAL_NAME_FILE_PROVISIONED, time_intervals: [] },
}, },
] ]
); );
@ -44,9 +49,9 @@ const getIntervalByName = (name: string) => {
}; };
export const listNamespacedTimeIntervalHandler = () => export const listNamespacedTimeIntervalHandler = () =>
http.get<{ namespace: string }>( http.get<{ namespace: string }, { fieldSelector: string }>(
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/timeintervals`, `${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/timeintervals`,
({ params }) => { ({ params, request }) => {
const { namespace } = params; const { namespace } = params;
// k8s APIs expect `default` rather than `org-1` - this is one particular example // k8s APIs expect `default` rather than `org-1` - this is one particular example
@ -59,6 +64,17 @@ export const listNamespacedTimeIntervalHandler = () =>
{ status: 403 } { status: 403 }
); );
} }
// Rudimentary filter support for `spec.name`
const url = new URL(request.url);
const fieldSelector = url.searchParams.get('fieldSelector');
if (fieldSelector && fieldSelector.includes('spec.name')) {
const filteredItems = filterBySelector(allTimeIntervals.items, fieldSelector);
return HttpResponse.json({ items: filteredItems });
}
return HttpResponse.json(allTimeIntervals); return HttpResponse.json(allTimeIntervals);
} }
); );

View File

@ -0,0 +1,20 @@
import { chain, filter, matchesProperty, trim } from 'lodash';
/**
* Filters a list of k8s items by a selector string
*/
export function filterBySelector<T>(items: T[], selector: string) {
// e.g. [['path.to.key', 'value'], ['other.path', 'value']]
const filters: string[][] = chain(selector)
.split(',')
.map(trim)
.map((s) => s.split('='))
.value();
return filter(items, (item) =>
filters.every(([key, value]) => {
const matcher = matchesProperty(key, value);
return matcher(item);
})
);
}

View File

@ -42,13 +42,6 @@ const injectedRtkApi = api
}), }),
invalidatesTags: ['TimeInterval'], invalidatesTags: ['TimeInterval'],
}), }),
readNamespacedTimeInterval: build.query<ReadNamespacedTimeIntervalApiResponse, ReadNamespacedTimeIntervalApiArg>({
query: (queryArg) => ({
url: `/apis/notifications.alerting.grafana.app/v0alpha1/namespaces/${queryArg['namespace']}/timeintervals/${queryArg.name}`,
params: { pretty: queryArg.pretty },
}),
providesTags: ['TimeInterval'],
}),
replaceNamespacedTimeInterval: build.mutation< replaceNamespacedTimeInterval: build.mutation<
ReplaceNamespacedTimeIntervalApiResponse, ReplaceNamespacedTimeIntervalApiResponse,
ReplaceNamespacedTimeIntervalApiArg ReplaceNamespacedTimeIntervalApiArg
@ -171,16 +164,6 @@ export type CreateNamespacedTimeIntervalApiArg = {
fieldValidation?: string; fieldValidation?: string;
comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval; comGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval: ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval;
}; };
export type ReadNamespacedTimeIntervalApiResponse =
/** status 200 OK */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval;
export type ReadNamespacedTimeIntervalApiArg = {
/** name of the TimeInterval */
name: string;
/** object name and auth scope, such as for teams and projects */
namespace: string;
/** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */
pretty?: string;
};
export type ReplaceNamespacedTimeIntervalApiResponse = /** status 200 OK */ export type ReplaceNamespacedTimeIntervalApiResponse = /** status 200 OK */
| ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval | ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval
| /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval; | /** status 201 Created */ ComGithubGrafanaGrafanaPkgApisAlertingNotificationsV0Alpha1TimeInterval;

View File

@ -31,7 +31,6 @@ const config: ConfigFile = {
filterEndpoints: [ filterEndpoints: [
'listNamespacedTimeInterval', 'listNamespacedTimeInterval',
'createNamespacedTimeInterval', 'createNamespacedTimeInterval',
'readNamespacedTimeInterval',
'deleteNamespacedTimeInterval', 'deleteNamespacedTimeInterval',
'patchNamespacedTimeInterval', 'patchNamespacedTimeInterval',
'replaceNamespacedTimeInterval', 'replaceNamespacedTimeInterval',