INFRA-3897 | Abhishek | Update delete resources flow for kubernetes resources

This commit is contained in:
Abhishek Katiyar
2024-11-11 00:05:41 +05:30
parent cbfd636221
commit cdbfdf71b5
8 changed files with 275 additions and 198 deletions

View File

@@ -1,9 +1,11 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Button,
Card,
Checkbox,
CircularProgress,
Dialog,
FormControlLabel,
Grid,
IconButton,
makeStyles,
@@ -11,17 +13,25 @@ import {
Typography,
} from '@material-ui/core';
import { getIn, useFormikContext } from 'formik';
import { httpClient, post } from '@src/helper/api-client';
import { httpClient, httpDelete, post } from '@src/helper/api-client';
import { toast } from 'react-toastify';
import { Done } from '@material-ui/icons';
import { CheckBox, Done } from '@material-ui/icons';
import DialogContent from '@material-ui/core/DialogContent';
import { API_TO_POST_MANIFEST } from '@src/constants/Endpoints';
import {
API_CREATE_REQUEST,
API_DELETE_K8S_RESOURCE,
API_GET_K8S_RESOURCE_STATUS,
API_TO_POST_MANIFEST,
DELETE_K8S_RESOURCE_API,
} from '@src/constants/Endpoints';
import { forEach } from 'lodash';
import { getFieldNameFromPath } from '@src/models/Manifest';
import { validate } from 'webpack';
interface DeleteResourceProps {
name: string;
fieldPath: string;
onDelete?: Function;
onDelete: Function;
disabled?: boolean;
children?: React.ReactNode;
index?: number;
@@ -29,17 +39,20 @@ interface DeleteResourceProps {
}
interface ConfirmationPopupProps {
handleDeleteResource: Function;
onDelete: Function;
k8sResourceName: string;
loading: boolean;
setLoading: Function;
isResourceDeployed: boolean;
deleteObjectEndpoint: string;
manifestId: number;
handleClose: Function;
openPopup: boolean;
loading: boolean;
setDeleted: Function;
confirmDelete: boolean;
setOpenPopup: Function;
}
interface NotifyDeleteProps {
fieldPath: string;
handleClose: Function;
}
const useStyles = makeStyles({
@@ -78,42 +91,96 @@ const useStyles = makeStyles({
},
});
const kubeObjectType = {
LoadBalancers: 'ingress',
'Service Monitor': 'serviceMonitor',
Deployment: 'deployment',
deployment: {
commonApiGateways: 'commonApiGateways',
loadBalancers: 'ingress',
hpa: {
cronJobs: 'cronHpa',
},
},
'Flink Job': 'flinkSessionJob',
'API Gateway': 'commonApiGateways',
};
const ConfirmationPopup = (props: ConfirmationPopupProps) => {
const classes = useStyles();
const { handleClose, handleDeleteResource } = props;
const { handleClose, onDelete } = props;
const [notifyDelete, setNotifyDelete] = useState(false);
const [deleteMessage, setDeleteMessage] = useState<string>();
let [deleteFromManifest, setDeleteFromManifest] = useState(false);
const { submitForm }: { submitForm: any } = useFormikContext();
const toggleDeleteFromManifest = () => {
setDeleteFromManifest(!deleteFromManifest);
};
const deleteK8sResource = () => {
props.setLoading(true);
httpDelete({}, props.deleteObjectEndpoint)
.then(res => {
props.setLoading(false);
setDeleteMessage('Successfully deleted Kubernetes resource');
setNotifyDelete(true);
})
.catch(error => {
props.setLoading(false);
toast.error(error.message);
});
};
const handleDelete = () => {
if (deleteFromManifest) {
onDelete();
submitForm();
} else {
deleteK8sResource();
}
};
const NotifyDelete = () => {
const classes = useStyles();
return (
<Card className={classes.card}>
<Grid container direction="row" alignItems="center" spacing={4}>
<Grid item xs={12}>
<Typography variant="h6" className={classes.cardContent}>
{deleteMessage}
</Typography>
</Grid>
<Grid item xs={12} className={classes.buttonRow}>
<Grid spacing={4}></Grid>
<Button
variant="outlined"
color="secondary"
className={classes.button}
onClick={() => {
props.handleClose();
}}
>
Close
</Button>
</Grid>
</Grid>
</Card>
);
};
const ConfirmDelete = () => {
return (
return notifyDelete ? (
<NotifyDelete />
) : (
<Grid container direction="row" alignItems="center" spacing={4}>
<Grid item xs={12}>
<Typography variant="h6" className={classes.cardContent}>
{`Proceed with deletion of kubernetes resource?`}{' '}
{`Proceed with deletion of kubernetes resource ?`}{' '}
</Typography>
</Grid>
<br />
<br />
<Grid item xs={12} className={classes.buttonRow}>
<Grid container spacing={4}></Grid>
<FormControlLabel
control={<Checkbox />}
label="Delete from manifest also"
onClick={toggleDeleteFromManifest}
checked={deleteFromManifest}
/>
<Grid item xs={8} spacing={2}>
<Button
variant="contained"
color="primary"
className={classes.button}
onClick={() => {
handleDeleteResource();
handleDelete();
}}
>
Yes
@@ -136,214 +203,159 @@ const ConfirmationPopup = (props: ConfirmationPopupProps) => {
return props.loading ? (
<CircularProgress className={classes.loader} />
) : props.confirmDelete ? (
) : props.isResourceDeployed ? (
<ConfirmDelete />
) : (
<></>
);
};
const NotifyDelete = (props: NotifyDeleteProps) => {
const classes = useStyles();
return (
<Card className={classes.card}>
<Grid container direction="row" alignItems="center" spacing={4}>
<Grid item xs={12}>
<Typography variant="h6" className={classes.cardContent}>
Resource successfully deleted from kubernetes.
</Typography>
</Grid>
<Grid item xs={12} className={classes.buttonRow}>
<Grid spacing={4}></Grid>
<Button
variant="outlined"
color="secondary"
className={classes.button}
onClick={() => {
props.handleClose();
}}
>
Close
</Button>
</Grid>
</Grid>
</Card>
);
const KubeObjectTypeMap = {
loadBalancers: {
name: 'ingress',
uniqueIdentifier: 'id',
},
commonApiGateways: {
name: 'commonApiGateways',
uniqueIdentifier: 'pathName',
},
};
const noop = (): void => undefined;
const DeleteImplementedMap = {
'deployment.loadBalancers': true,
deployment: true,
'deployment.commonApiGateways': true,
'deployment.hpa.croHpa': true,
'deployment.serviceMonitor': true,
};
const DeleteResource = ({ onDelete = noop, ...props }: DeleteResourceProps) => {
const DeleteResource = (props: DeleteResourceProps) => {
const [openPopup, setOpenPopup] = useState(false);
const deleteButtonTooltip = props.tooltip ? props.tooltip : 'Delete';
const name = props.name;
const { values }: { values: any } = useFormikContext();
const deleteButtonTooltip = props.tooltip ? props.tooltip : 'Delete from Manifest or Kubernetes';
const { values, submitForm }: { values: any; submitForm: any } = useFormikContext();
const manifestId = values?.id;
const [deleted, setDeleted] = useState(false);
const [confirmDelete, setConfirmDelete] = useState(false);
const [isResourceDeployed, setIsResourceDeployed] = useState(false);
const [loading, setLoading] = useState(false);
const kubeObjectKind = getIn(kubeObjectType, name);
const kubeObject = getIn(values, props.name);
const deleteItemAndSaveManifest = (fieldPath: string) => {
console.error('Path not found', fieldPath);
const keys = fieldPath.split('.');
let current = Object.assign({}, values);
if (keys.length == 1) {
delete current[keys[0]];
console.log(current);
} else {
for (let i = 1; i < keys.length; i++) {
current = current[keys[i - 1]];
}
delete current[keys[keys.length - 1]];
}
post(current, API_TO_POST_MANIFEST).then(res => {
if (!res.ok) {
throw new Error('Error submitting manifest.');
} else {
location.reload();
}
});
};
function getPathAndIndex(fieldPath) {
const match = fieldPath.match(/\.(\d+)$/);
const index = match ? parseInt(match[1], 10) : undefined;
const pathWithoutIndex = match ? fieldPath.slice(0, fieldPath.lastIndexOf('.')) : fieldPath;
return {
index: index,
pathWithoutIndex: pathWithoutIndex,
};
}
const deleteWithToastOnError = (fieldPath: string): void => {
try {
deleteItemAndSaveManifest(fieldPath);
onDelete();
setDeleted(false);
setOpenPopup(false);
} catch (err) {
toast.error(err.message);
}
};
const { index, pathWithoutIndex }: { index: any; pathWithoutIndex: string } = getPathAndIndex(
props.fieldPath,
);
const fieldName = getFieldNameFromPath(pathWithoutIndex);
let k8sResourceName = fieldName;
if (KubeObjectTypeMap.hasOwnProperty(fieldName)) {
k8sResourceName = KubeObjectTypeMap[fieldName]['name'];
}
let getObjectEndpoint = API_GET_K8S_RESOURCE_STATUS(
manifestId,
k8sResourceName,
undefined,
undefined,
);
let deleteObjectEndpoint = API_DELETE_K8S_RESOURCE(
manifestId,
k8sResourceName,
undefined,
undefined,
);
const handleDelete = () => {
setOpenPopup(true);
if (kubeObjectKind !== undefined && manifestId !== undefined) {
if (index !== undefined) {
k8sResourceName = KubeObjectTypeMap[fieldName]['name'];
let uniqueIdentifierName =
KubeObjectTypeMap[fieldName]['uniqueIdentifierName'] !== undefined
? KubeObjectTypeMap[fieldName]['uniqueIdentifierName']
: fieldName;
let uniqueIdentiferValue =
getIn(values, props.fieldPath) !== undefined
? getIn(values, props.fieldPath)[uniqueIdentifierName]
: undefined;
getObjectEndpoint = API_GET_K8S_RESOURCE_STATUS(
manifestId,
k8sResourceName,
uniqueIdentifierName,
uniqueIdentiferValue,
);
deleteObjectEndpoint = API_GET_K8S_RESOURCE_STATUS(
manifestId,
k8sResourceName,
uniqueIdentifierName,
uniqueIdentiferValue,
);
}
const checkIfDeployed = () => {
if (manifestId !== undefined) {
setOpenPopup(true);
setLoading(true);
let getObjectEndpoint = `/api/kube/manifest/${values.id}/kubeObjectType/${kubeObjectKind}`;
if (props.name === 'deployment.loadBalancers' && props.index !== undefined) {
const resourceId = getIn(kubeObject[props.index], 'id');
if (resourceId === undefined) {
deleteWithToastOnError(props.fieldPath);
setOpenPopup(false);
return;
}
getObjectEndpoint = `/api/kube/manifest/${values.id}/kubeObjectType/${kubeObjectKind}?resourceId=${resourceId}`;
}
httpClient(getObjectEndpoint)
.then(res => {
res
.json()
.then(respJson => {
if (res.ok) {
if (res.ok) {
res
.json()
.then(respJson => {
if (respJson === true) {
setConfirmDelete(true);
setLoading(false);
setIsResourceDeployed(true);
} else {
setOpenPopup(false);
deleteWithToastOnError(props.fieldPath);
props.onDelete();
}
} else {
toast.error(`${respJson.message}`);
})
.catch(err => {
toast.error(`${err.message}`);
setOpenPopup(false);
}
})
.catch(resJson => {
toast.error(
'Could not check if resource is deployed on kubernetes, please reach out to Cloud-Platform team',
resJson.statusText,
);
setOpenPopup(false);
});
});
} else {
setLoading(false);
toast.error(
'Could not check if resource is deployed on kubernetes, please reach out to Cloud-Platform team',
resJson.statusText,
);
}
})
.finally(() => {
setLoading(false);
.catch(resJson => {
toast.error(
'Could not check if resource is deployed on kubernetes, please reach out to Cloud-Platform team',
resJson.statusText,
);
setOpenPopup(false);
});
} else {
// item is only in the UI and not in the DB
deleteWithToastOnError(props.fieldPath);
setOpenPopup(false);
setIsResourceDeployed(false);
}
};
const deleteResource = () => {
let deleteEndpoint = getDeleteEndpoint(values, kubeObjectKind, props, kubeObject);
setLoading(true);
httpClient(deleteEndpoint, { method: 'DELETE' })
.then(res => {
if (res.ok) {
setDeleted(true);
} else {
setLoading(false);
setOpenPopup(false);
toast.error(`Unable to delete resource : ${res.statusText} ${res.status}`);
setOpenPopup(false);
}
})
.catch(error => {
setLoading(false);
toast.error(`API call failed with error ${error}`);
});
};
const getDeleteEndpoint = (
values: any,
kubeObjectKind: any,
props: {
name: string;
disabled?: boolean;
children?: React.ReactNode;
index?: number;
tooltip?: string;
},
kubeObject: any,
) => {
let deleteEndpoint = `/api/kube/manifest/${values.id}/kubeObjectType/${kubeObjectKind}`;
if (props.name === 'deployment.loadBalancers' && props.index !== undefined) {
deleteEndpoint = `/api/kube/manifest/${
values.id
}/kubeObjectType/${kubeObjectKind}?resourceId=${getIn(kubeObject[props.index], 'id')}`;
} else if (
props.name === 'deployment.commonApiGateways.0.gatewayAttributes' &&
props.index !== undefined
) {
deleteEndpoint = `/api/kube/manifest/${
values.id
}/kubeObjectType/${kubeObjectKind}?apiGatewayPathName=${getIn(
kubeObject[props.index],
'pathName',
)}`;
}
return deleteEndpoint;
};
const handleClosePopup = () => {
deleteWithToastOnError(props.fieldPath);
};
return (
<>
<Dialog open={openPopup}>
<DialogContent style={{ overflow: 'hidden' }}>
<ConfirmationPopup
loading={loading}
openPopup={openPopup}
setDeleted={setDeleted}
confirmDelete={confirmDelete}
handleDeleteResource={deleteResource}
setOpenPopup={setOpenPopup}
onDelete={props.onDelete}
loading={loading}
setLoading={setLoading}
manifestId={manifestId}
isResourceDeployed={isResourceDeployed}
k8sResourceName={k8sResourceName}
deleteObjectEndpoint={deleteObjectEndpoint}
handleClose={() => {
setOpenPopup(false);
}}
/>
</DialogContent>
</Dialog>
<Dialog open={deleted}>
<NotifyDelete handleClose={() => handleClosePopup()} fieldPath={props.fieldPath} />
</Dialog>
<Tooltip title={deleteButtonTooltip}>
<IconButton disabled={props.disabled} onClick={handleDelete}>
<IconButton disabled={props.disabled} onClick={checkIfDeployed}>
{props.children}
</IconButton>
</Tooltip>

View File

@@ -4,6 +4,7 @@ import * as React from 'react';
import AddIcon from '@material-ui/icons/Add';
import DeleteIcon from '@material-ui/icons/Delete';
import DeleteResource from './DeleteResource';
import { path } from '@src/models/Manifest';
const useStyles = makeStyles({
root: {
@@ -57,6 +58,7 @@ export const FormikCardList = (props: FormikCardListProps) => {
<Grid container justifyContent="space-between" direction={direction}>
<DeleteResource
name={name}
fieldPath={`${path.loadBalancers}.${i}`}
onDelete={deleteItem}
disabled={removeCondition?.[i]}
index={i}

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import Button, { ButtonProps } from '@material-ui/core/Button';
import RemoveIcon from '@material-ui/icons/Remove';
import DeleteResource from './DeleteResource';
import { useFormikContext } from 'formik';
export interface RemoveButtonProps extends ButtonProps {
name: string;
@@ -13,6 +14,7 @@ export interface RemoveButtonProps extends ButtonProps {
const RemoveButton = (props: RemoveButtonProps) => {
const { removeAction, ...buttonProps } = props;
return (
<DeleteResource
name={props.name}

View File

@@ -1,3 +1,5 @@
import error from 'material-ui/svg-icons/alert/error';
export const API_TO_POST_MANIFEST = '/api/manifest';
export const API_CREATE_REQUEST = '/api/changeRequest';
export const API_TO_EXPORT_MANIFEST = (manifestId: string, version): string =>
@@ -6,3 +8,43 @@ export const API_TO_EXPORT_MANIFEST = (manifestId: string, version): string =>
: `/api/manifest/${manifestId}/export`;
export const API_TO_MANAGE_ARGO_ROLLOUT = (manifestId: number, operation: string): string =>
`/api/kube/manifest/${manifestId}/rollout/${operation}`;
export const API_TO_DELETE_INGRESS = (
manifestId: number,
kubeObjectKind: string,
resourceId: number,
): string =>
resourceId
? `/api/kube/manifest/${manifestId}/kubeObjectKind/${kubeObjectKind}?resourceId=${resourceId}`
: `/api/kube/manifest/${manifestId}/kubeObjectKind/${kubeObjectKind}?resourceId=${resourceId}`;
export const API_TO_DELETE_COMMON_API_GATEWAY = (
manifestId: number,
kubeObjectKind: string,
apiGatewayPathName: string,
): string =>
apiGatewayPathName
? `/api/kube/manifest/${manifestId}/kubeObjectKind/${kubeObjectKind}/apiGatewayPathName?apiGatewayPathName=${apiGatewayPathName}`
: `/api/kube/manifest/${manifestId}/kubeObjectKind/${kubeObjectKind}/apiGatewayPathName?apiGatewayPathName=${apiGatewayPathName}`;
export const API_GET_K8S_RESOURCE_STATUS = (
manifestId: number,
k8sResourceName: string,
uniqueIdentifierName: string | undefined,
uniqueIdentiferValue: any,
) => {
if (uniqueIdentifierName !== undefined && uniqueIdentiferValue !== undefined) {
return `/api/kube/manifest/${manifestId}/kubeObjectType/${k8sResourceName}?${uniqueIdentifierName}=${uniqueIdentiferValue}`;
}
return `/api/kube/manifest/${manifestId}/kubeObjectType/${k8sResourceName}`;
};
export const API_DELETE_K8S_RESOURCE = (
manifestId: number,
k8sResourceName: string,
uniqueIdentifierName: string | undefined,
uniqueIdentifierValue: any,
) => {
if (uniqueIdentifierName !== undefined && uniqueIdentifierValue !== undefined) {
return `/api/kube/manifest/${manifestId}/kubeObjectType/${k8sResourceName}?${uniqueIdentifierName}=${uniqueIdentifierValue}`;
}
return `/api/kube/manifest/${manifestId}/kubeObjectType/${k8sResourceName}`;
};

View File

@@ -382,7 +382,6 @@ const LoadBalancerBasicTab: FC = () => {
name="deployment.loadBalancers"
newItem={_m.newLoadBalancer}
newItemLabel="Add LoadBalancer"
deleteItemFn={deleteItemAndSaveManifest}
>
{(i: number) => (
<>

View File

@@ -382,7 +382,11 @@ const FlinkJob = (): React.JSX.Element => {
name="flink.flinkJob.entryClass"
></FormikTextField>
<div className={'delete-btn-container'}>
<DeleteResource name={'Flink Job'} tooltip="Deletes flink job from Kubernetes">
<DeleteResource
onDelete={() => {}}
name={'Flink Job'}
tooltip="Deletes flink job from Kubernetes"
>
<DeleteIcon />
</DeleteResource>
</div>

View File

@@ -47,3 +47,10 @@ export const post = async (values: any, apiPath: string): Promise<any> => {
body: JSON.stringify(values),
});
};
export const httpDelete = async (values: any, apiPath: string): Promise<any> => {
return httpClient(apiPath, {
method: 'DELETE',
body: JSON.stringify(values),
});
};

View File

@@ -49,6 +49,15 @@ export const path = {
scylladb: 'deployment.scyllaDb',
};
export const getFieldNameFromPath = (fieldPath: string): string => {
for (const [key, value] of Object.entries(path)) {
if (value === fieldPath) {
return key;
}
}
return fieldPath;
};
// Constants
const commonNamespaces = ['perf', 'monitoring', 'infrastructure', 'airflow', 'data-platform'];
export const namespaces = {