add the pod delete feature to the kubernetes react plugin (#24534)

* add the pod delete feature to the kubernetes react plugin

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* move podDelete into PodDrawer

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* remove dialog and use only a button

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* move config inside frontend object

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* update the k8 configration.md documentation

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* add error ui if error

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* refactor restart text logic

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* fix test var name

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* fix doc key

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* revert last commit

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* add i18n

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* Update plugins/kubernetes-react/src/translation.ts

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Signed-off-by: Seba <17096352+sebalaini@users.noreply.github.com>

* remove buttonText config

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* add alpha and doc

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

* add alpha api report

Signed-off-by: sebalaini <sebastianolaini@gmail.com>

---------

Signed-off-by: sebalaini <sebastianolaini@gmail.com>
Signed-off-by: Seba <17096352+sebalaini@users.noreply.github.com>
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Seba
2024-11-05 21:04:48 +01:00
committed by GitHub
parent e17b82c83b
commit 0b729da922
20 changed files with 500 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-kubernetes-react': patch
---
add the pod delete feature to the kubernetes react plugin
+52
View File
@@ -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.
+15
View File
@@ -27,5 +27,20 @@ export interface Config {
*/
enabled?: boolean;
};
/**
* Frontend config
*/
frontend?: {
/**
* Pod Delete config
*/
podDelete?: {
/**
* Enable `podDelete` UI feature
* @visibility frontend
*/
enabled?: boolean;
};
};
};
}
+16 -3
View File
@@ -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"
@@ -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)
```
+20
View File
@@ -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<KubernetesProxyApi>;
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,
+19
View File
@@ -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 };
@@ -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 }));
}
}
@@ -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;
@@ -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 (
<ListItem key={event.metadata.name}>
<ListItem key={`${event.metadata.name}-${index}`}>
<Tooltip title={`${event.type ?? ''} event`}>
{getAvatarByType(event.type)}
</Tooltip>
@@ -21,13 +21,13 @@ import { renderInTestApp } from '@backstage/test-utils';
jest.mock('../Events', () => ({
Events: () => {
return <React.Fragment data-testid="events" />;
return <React.Fragment />;
},
}));
jest.mock('../PodLogs', () => ({
PodLogs: () => {
return <React.Fragment data-testid="logs" />;
return <React.Fragment />;
},
}));
@@ -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 (
<Grid container item xs={12}>
<Grid item xs={12}>
<CardActions>
<Button
variant="outlined"
aria-label={buttonText}
component="label"
onClick={handleDeleteClick}
startIcon={
isLoading ? <CircularProgress size={18} /> : <DeleteIcon />
}
disabled={isLoading}
>
{buttonText}
</Button>
</CardActions>
{hasError && (
<Typography
variant="body1"
color="error"
style={{ textAlign: 'right' }}
>
Could not delete the pod. Please check the console for the full
report.
</Typography>
)}
</Grid>
</Grid>
);
};
@@ -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';
@@ -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;
}
@@ -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;
};
@@ -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 (
<KubernetesDrawer
@@ -95,6 +98,15 @@ export const PodDrawer = ({ podAndErrors, open }: PodDrawerProps) => {
}
>
<div className={classes.content}>
{isPodDeleteEnabled && (
<PodDeleteButton
podScope={{
podName: podAndErrors.pod.metadata?.name ?? 'unknown',
podNamespace: podAndErrors.pod.metadata?.namespace ?? 'default',
cluster: podAndErrors.cluster,
}}
/>
)}
{podMetrics && (
<Grid container item xs={12}>
<Grid item xs={12}>
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './useIsPodDeleteEnabled';
export * from './useIsPodExecTerminalEnabled';
export * from './useIsPodExecTerminalSupported';
export * from './useKubernetesObjects';
@@ -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) => (
<TestApiProvider
apis={[
[
configApiRef,
new ConfigReader({
kubernetes: {
frontend: {
podDelete: { enabled: isPodDeleteEnabled },
},
},
}),
],
]}
>
{children}
</TestApiProvider>
);
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);
});
});
@@ -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');
};
@@ -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',
},
},
},
});