diff --git a/.changeset/healthy-bags-jog.md b/.changeset/healthy-bags-jog.md new file mode 100644 index 0000000000..ab4930963c --- /dev/null +++ b/.changeset/healthy-bags-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-react': patch +--- + +add the pod delete feature to the kubernetes react plugin diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 7c14c40283..71fdd0be0e 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -17,6 +17,9 @@ The following is a full example entry in `app-config.yaml`: ```yaml kubernetes: + frontend: + podDelete: + enabled: true serviceLocatorMethod: type: 'multiTenant' clusterLocatorMethods: @@ -48,6 +51,55 @@ kubernetes: exposeDashboard: true ``` +### `frontend` (optional) + +This is an array used to configure some frontend features. + +Valid values are: + +- `podDelete` + +#### `podDelete` (optional) + +This configures the behavior of the delete pod button in the container panel. + +Valid configurations are: + +- `enabled` + +##### `enabled` + +This configuration controls the visibility of this feature. + +Valid values are: + +- `true` +- `false` + +The default value is `false`. + +#### Internationalization + +To customize or translate the **Delete Pod** text, use the following approach: + +```js +import { createTranslationMessages } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '@backstage/plugin-kubernetes-react/alpha'; + +const app = createApp({ + __experimentalTranslations: { + resources: [ + createTranslationMessages({ + ref: kubernetesReactTranslationRef, + messages: { + "podDrawer.buttons.delete": 'Restart Pod' + } + }) + ] + }, + ... +``` + ### `serviceLocatorMethod` This configures how to determine which clusters a component is running in. diff --git a/plugins/kubernetes-react/config.d.ts b/plugins/kubernetes-react/config.d.ts index 2d2b790d05..b5b811609b 100644 --- a/plugins/kubernetes-react/config.d.ts +++ b/plugins/kubernetes-react/config.d.ts @@ -27,5 +27,20 @@ export interface Config { */ enabled?: boolean; }; + /** + * Frontend config + */ + frontend?: { + /** + * Pod Delete config + */ + podDelete?: { + /** + * Enable `podDelete` UI feature + * @visibility frontend + */ + enabled?: boolean; + }; + }; }; } diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 2d8430b953..611766a640 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -14,9 +14,7 @@ ] }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "repository": { "type": "git", @@ -25,8 +23,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist", "config.d.ts" diff --git a/plugins/kubernetes-react/report-alpha.api.md b/plugins/kubernetes-react/report-alpha.api.md new file mode 100644 index 0000000000..00a8b6b819 --- /dev/null +++ b/plugins/kubernetes-react/report-alpha.api.md @@ -0,0 +1,17 @@ +## API Report File for "@backstage/plugin-kubernetes-react" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; + +// @alpha (undocumented) +export const kubernetesReactTranslationRef: TranslationRef< + 'kubernetes-react', + { + readonly 'podDrawer.buttons.delete': 'Delete Pod'; + } +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/kubernetes-react/report.api.md b/plugins/kubernetes-react/report.api.md index adb75a7d60..454302f632 100644 --- a/plugins/kubernetes-react/report.api.md +++ b/plugins/kubernetes-react/report.api.md @@ -512,6 +512,14 @@ export interface KubernetesObjects { // @public (undocumented) export interface KubernetesProxyApi { + // (undocumented) + deletePod(request: { + podName: string; + namespace: string; + clusterName: string; + }): Promise<{ + text: string; + }>; // (undocumented) getEventsByInvolvedObjectName(request: { clusterName: string; @@ -537,6 +545,18 @@ export const kubernetesProxyApiRef: ApiRef; export class KubernetesProxyClient { constructor(options: { kubernetesApi: KubernetesApi }); // (undocumented) + deletePod({ + podName, + namespace, + clusterName, + }: { + podName: string; + namespace: string; + clusterName: string; + }): Promise<{ + text: string; + }>; + // (undocumented) getEventsByInvolvedObjectName({ clusterName, involvedObjectName, diff --git a/plugins/kubernetes-react/src/alpha.ts b/plugins/kubernetes-react/src/alpha.ts new file mode 100644 index 0000000000..411bd78878 --- /dev/null +++ b/plugins/kubernetes-react/src/alpha.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { kubernetesReactTranslationRef } from './translation'; + +export { kubernetesReactTranslationRef }; diff --git a/plugins/kubernetes-react/src/api/KubernetesProxyClient.ts b/plugins/kubernetes-react/src/api/KubernetesProxyClient.ts index 3703d8b180..caad2cafd0 100644 --- a/plugins/kubernetes-react/src/api/KubernetesProxyClient.ts +++ b/plugins/kubernetes-react/src/api/KubernetesProxyClient.ts @@ -110,4 +110,25 @@ export class KubernetesProxyClient { .then(response => this.handleText(response)) .then(text => ({ text })); } + + async deletePod({ + podName, + namespace, + clusterName, + }: { + podName: string; + namespace: string; + clusterName: string; + }): Promise<{ text: string }> { + return await this.kubernetesApi + .proxy({ + clusterName: clusterName, + path: `/api/v1/namespaces/${namespace}/pods/${podName}`, + init: { + method: 'DELETE', + }, + }) + .then(response => this.handleText(response)) + .then(text => ({ text })); + } } diff --git a/plugins/kubernetes-react/src/api/types.ts b/plugins/kubernetes-react/src/api/types.ts index cc27bf9dd4..548a2e150c 100644 --- a/plugins/kubernetes-react/src/api/types.ts +++ b/plugins/kubernetes-react/src/api/types.ts @@ -83,6 +83,11 @@ export interface KubernetesProxyApi { containerName: string; previous?: boolean; }): Promise<{ text: string }>; + deletePod(request: { + podName: string; + namespace: string; + clusterName: string; + }): Promise<{ text: string }>; getEventsByInvolvedObjectName(request: { clusterName: string; involvedObjectName: string; diff --git a/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx b/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx index 6a43b2fe5b..bc5d797394 100644 --- a/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx +++ b/plugins/kubernetes-react/src/components/Pods/Events/Events.tsx @@ -76,7 +76,7 @@ export const EventsContent = ({ } return true; }) - .map(event => { + .map((event, index) => { const timeAgo = event.metadata.creationTimestamp ? DateTime.fromISO(event.metadata.creationTimestamp).toRelative( { @@ -85,7 +85,7 @@ export const EventsContent = ({ ) : 'unknown'; return ( - + {getAvatarByType(event.type)} diff --git a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx index 7972f4401c..25cccbb9c6 100644 --- a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.test.tsx @@ -21,13 +21,13 @@ import { renderInTestApp } from '@backstage/test-utils'; jest.mock('../Events', () => ({ Events: () => { - return ; + return ; }, })); jest.mock('../PodLogs', () => ({ PodLogs: () => { - return ; + return ; }, })); diff --git a/plugins/kubernetes-react/src/components/Pods/PodDelete/PodDeleteButton.tsx b/plugins/kubernetes-react/src/components/Pods/PodDelete/PodDeleteButton.tsx new file mode 100644 index 0000000000..622a7cba63 --- /dev/null +++ b/plugins/kubernetes-react/src/components/Pods/PodDelete/PodDeleteButton.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; + +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import CardActions from '@material-ui/core/CardActions'; +import Button from '@material-ui/core/Button'; +import DeleteIcon from '@material-ui/icons/Close'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { kubernetesReactTranslationRef } from '../../../translation'; + +import { usePodDelete } from './usePodDelete'; +import { PodScope } from './types'; + +/** + * Props for PodDeleteButton + * + * @public + */ +export interface PodDeleteButtonProps { + podScope: PodScope; +} + +/** + * a Delete button to delete a given pod + * + * @public + */ +export const PodDeleteButton = ({ podScope }: PodDeleteButtonProps) => { + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const deletePod = usePodDelete(); + + const { t } = useTranslationRef(kubernetesReactTranslationRef); + const buttonText = t('podDrawer.buttons.delete'); + + const handleDeleteClick = async () => { + setIsLoading(true); + try { + await deletePod(podScope); + } catch (error) { + setHasError(true); + // eslint-disable-next-line no-console + console.error(error); + } + }; + + return ( + + + + + + {hasError && ( + + Could not delete the pod. Please check the console for the full + report. + + )} + + + ); +}; diff --git a/plugins/kubernetes-react/src/components/Pods/PodDelete/index.ts b/plugins/kubernetes-react/src/components/Pods/PodDelete/index.ts new file mode 100644 index 0000000000..443f758a29 --- /dev/null +++ b/plugins/kubernetes-react/src/components/Pods/PodDelete/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './PodDeleteButton'; +export * from './usePodDelete'; +export * from './types'; diff --git a/plugins/kubernetes-react/src/components/Pods/PodDelete/types.ts b/plugins/kubernetes-react/src/components/Pods/PodDelete/types.ts new file mode 100644 index 0000000000..8b277371dd --- /dev/null +++ b/plugins/kubernetes-react/src/components/Pods/PodDelete/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ClusterAttributes } from '@backstage/plugin-kubernetes-common'; + +/** + * Contains the details needed to make a delete request to Kubernetes + * + * @public + */ +export interface PodScope { + podName: string; + podNamespace: string; + cluster: ClusterAttributes; +} diff --git a/plugins/kubernetes-react/src/components/Pods/PodDelete/usePodDelete.ts b/plugins/kubernetes-react/src/components/Pods/PodDelete/usePodDelete.ts new file mode 100644 index 0000000000..31ac2478b8 --- /dev/null +++ b/plugins/kubernetes-react/src/components/Pods/PodDelete/usePodDelete.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useCallback } from 'react'; + +import { PodScope } from './types'; +import { useApi } from '@backstage/core-plugin-api'; +import { kubernetesProxyApiRef } from '../../../api/types'; + +/** + * Arguments for usePodDelete + * + * @public + */ +export interface PodDeleteOptions { + podScope: PodScope; +} + +/** + * Delete a given pod + * + * @public + */ +export const usePodDelete = () => { + const kubernetesProxyApi = useApi(kubernetesProxyApiRef); + + const deletePod = useCallback( + async (podScope: PodScope) => { + return await kubernetesProxyApi.deletePod({ + podName: podScope.podName, + namespace: podScope.podNamespace, + clusterName: podScope.cluster.name, + }); + }, + [kubernetesProxyApi], + ); + + return deletePod; +}; diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx index 7a7b17b7f6..3709b1186e 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.tsx @@ -27,11 +27,13 @@ import { ContainerCard } from './ContainerCard'; import { PodAndErrors } from '../types'; import { KubernetesDrawer } from '../../KubernetesDrawer'; +import { PodDeleteButton } from '../PodDelete/PodDeleteButton'; import { PendingPodContent } from './PendingPodContent'; import { ErrorList } from '../ErrorList'; import { usePodMetrics } from '../../../hooks/usePodMetrics'; import { ResourceUtilization } from '../../ResourceUtilization'; import { bytesToMiB, formatMillicores } from '../../../utils/resources'; +import { useIsPodDeleteEnabled } from '../../../hooks'; const useDrawerContentStyles = makeStyles((_theme: Theme) => createStyles({ @@ -76,6 +78,7 @@ export interface PodDrawerProps { export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => { const classes = useDrawerContentStyles(); const podMetrics = usePodMetrics(podAndErrors.cluster.name, podAndErrors.pod); + const isPodDeleteEnabled = useIsPodDeleteEnabled(); return ( { } >
+ {isPodDeleteEnabled && ( + + )} {podMetrics && ( diff --git a/plugins/kubernetes-react/src/hooks/index.ts b/plugins/kubernetes-react/src/hooks/index.ts index 2bc519d69a..7f80e95e41 100644 --- a/plugins/kubernetes-react/src/hooks/index.ts +++ b/plugins/kubernetes-react/src/hooks/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './useIsPodDeleteEnabled'; export * from './useIsPodExecTerminalEnabled'; export * from './useIsPodExecTerminalSupported'; export * from './useKubernetesObjects'; diff --git a/plugins/kubernetes-react/src/hooks/useIsPodDeleteEnabled.test.tsx b/plugins/kubernetes-react/src/hooks/useIsPodDeleteEnabled.test.tsx new file mode 100644 index 0000000000..92aa601d8d --- /dev/null +++ b/plugins/kubernetes-react/src/hooks/useIsPodDeleteEnabled.test.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ConfigReader } from '@backstage/core-app-api'; +import { configApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; +import { renderHook } from '@testing-library/react'; +import { PropsWithChildren } from 'react'; +import React from 'react'; + +import { useIsPodDeleteEnabled } from './useIsPodDeleteEnabled'; + +describe('useIsPodDeleteEnabled', () => { + let isPodDeleteEnabled: boolean | undefined; + + const apiWrapper = ({ children }: PropsWithChildren) => ( + + {children} + + ); + + it.each([ + { + condition: 'missing config', + returnValue: undefined, + }, + { condition: 'disabled', returnValue: false }, + { + condition: 'enabled', + returnValue: true, + }, + ])('Should return $returnValue if $condition', async ({ returnValue }) => { + isPodDeleteEnabled = returnValue; + + const { result } = renderHook(() => useIsPodDeleteEnabled(), { + wrapper: apiWrapper, + }); + + expect(result.current).toEqual(returnValue); + }); +}); diff --git a/plugins/kubernetes-react/src/hooks/useIsPodDeleteEnabled.ts b/plugins/kubernetes-react/src/hooks/useIsPodDeleteEnabled.ts new file mode 100644 index 0000000000..7762a050ac --- /dev/null +++ b/plugins/kubernetes-react/src/hooks/useIsPodDeleteEnabled.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { configApiRef, useApi } from '@backstage/core-plugin-api'; + +/** + * Check if conditions for a pod delete call through the proxy endpoint are met + * + * @internal + */ +export const useIsPodDeleteEnabled = (): boolean | undefined => { + const configApi = useApi(configApiRef); + + return configApi + .getOptionalConfig('kubernetes.frontend') + ?.getOptionalBoolean('podDelete.enabled'); +}; diff --git a/plugins/kubernetes-react/src/translation.ts b/plugins/kubernetes-react/src/translation.ts new file mode 100644 index 0000000000..5b8a26b0d5 --- /dev/null +++ b/plugins/kubernetes-react/src/translation.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const kubernetesReactTranslationRef = createTranslationRef({ + id: 'kubernetes-react', + messages: { + podDrawer: { + buttons: { + delete: 'Delete Pod', + }, + }, + }, +});