diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index 268f908e6e..e13f3af882 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -234,6 +234,17 @@ func serveWithNoCacheHeader(fs http.Handler) http.HandlerFunc { } } +// defaultKubeConfigFile returns the default path to the kubeconfig file. +func defaultKubeConfigFile() (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get user home directory: %v", err) + } + + kubeConfigFile := filepath.Join(homeDir, ".kube", "config") + return kubeConfigFile, nil +} + // defaultKubeConfigPersistenceDir returns the default directory to store kubeconfig // files of clusters that are loaded in Headlamp. func defaultKubeConfigPersistenceDir() (string, error) { @@ -1384,6 +1395,26 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { return } + removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true" + + if removeKubeConfig { + // delete context from actual deafult kubecofig file + kubeConfigFile, err := defaultKubeConfigFile() + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": name}, err, "failed to get default kubeconfig file path") + http.Error(w, "failed to get default kubeconfig file path", http.StatusInternalServerError) + return + } + + // Use kubeConfigFile to remove the context from the default kubeconfig file + err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": name}, err, "removing context from default kubeconfig file") + http.Error(w, "removing context from default kubeconfig file", http.StatusInternalServerError) + return + } + } + kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile() if err != nil { logger.Log(logger.LevelError, map[string]string{"cluster": name}, @@ -1396,8 +1427,7 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { logger.Log(logger.LevelInfo, map[string]string{ "cluster": name, "kubeConfigPersistenceFile": kubeConfigPersistenceFile, - }, - nil, "Removing cluster from kubeconfig") + }, nil, "Removing cluster from kubeconfig") err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile) if err != nil { diff --git a/frontend/src/components/App/Home/index.tsx b/frontend/src/components/App/Home/index.tsx index 94c5e95159..776496b4c9 100644 --- a/frontend/src/components/App/Home/index.tsx +++ b/frontend/src/components/App/Home/index.tsx @@ -25,6 +25,24 @@ import { ConfirmDialog } from '../../common'; import ResourceTable from '../../common/Resource/ResourceTable'; import RecentClusters from './RecentClusters'; +/** + * Gets the origin of a cluster. + * + * @param cluster + * @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file. + */ +function getOrigin(cluster: Cluster): string { + if (cluster.meta_data?.source === 'kubeconfig') { + const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config'; + return `Kubeconfig: ${kubeconfigPath}`; + } else if (cluster.meta_data?.source === 'dynamic_cluster') { + return t('translation|Plugin'); + } else if (cluster.meta_data?.source === 'in_cluster') { + return t('translation|In-cluster'); + } + return 'Unknown'; +} + function ContextMenu({ cluster }: { cluster: Cluster }) { const { t } = useTranslation(['translation']); const history = useHistory(); @@ -33,8 +51,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { const menuId = useId('context-menu'); const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false); - function removeCluster(cluster: Cluster) { - deleteCluster(cluster.name || '') + function removeCluster(cluster: Cluster, removeKubeconfig?: boolean) { + deleteCluster(cluster.name || '', removeKubeconfig) .then(config => { dispatch(setConfig(config)); }) @@ -92,7 +110,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { > {t('translation|Settings')} - {helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && ( + + {helpers.isElectron() && ( { setOpenConfirmDialog(true); @@ -109,13 +128,18 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { handleClose={() => setOpenConfirmDialog(false)} onConfirm={() => { setOpenConfirmDialog(false); - removeCluster(cluster); + if (cluster.meta_data?.source !== 'dynamic_cluster') { + removeCluster(cluster, true); + } else { + removeCluster(cluster); + } }} title={t('translation|Delete Cluster')} description={t( - 'translation|Are you sure you want to remove the cluster "{{ clusterName }}"?', + 'translation|This action will delete cluster "{{ clusterName }}" from {{ source }}.', { clusterName: cluster.name, + source: getOrigin(cluster), } )} /> @@ -239,24 +263,6 @@ function HomeComponent(props: HomeComponentProps) { .sort(); } - /** - * Gets the origin of a cluster. - * - * @param cluster - * @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file. - */ - function getOrigin(cluster: Cluster): string { - if (cluster.meta_data?.source === 'kubeconfig') { - const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config'; - return `Kubeconfig: ${kubeconfigPath}`; - } else if (cluster.meta_data?.source === 'dynamic_cluster') { - return t('translation|Plugin'); - } else if (cluster.meta_data?.source === 'in_cluster') { - return t('translation|In-cluster'); - } - return 'Unknown'; - } - const memoizedComponent = React.useMemo( () => ( diff --git a/frontend/src/components/common/ConfirmDialog.tsx b/frontend/src/components/common/ConfirmDialog.tsx index a503fd43d9..0a7317c15a 100644 --- a/frontend/src/components/common/ConfirmDialog.tsx +++ b/frontend/src/components/common/ConfirmDialog.tsx @@ -9,7 +9,7 @@ import { DialogTitle } from './Dialog'; export interface ConfirmDialogProps extends MuiDialogProps { title: string; - description: string; + description: string | JSX.Element; onConfirm: () => void; handleClose: () => void; } diff --git a/frontend/src/lib/k8s/api/v1/clusterApi.ts b/frontend/src/lib/k8s/api/v1/clusterApi.ts index 374a2d59bd..ccff6b88c7 100644 --- a/frontend/src/lib/k8s/api/v1/clusterApi.ts +++ b/frontend/src/lib/k8s/api/v1/clusterApi.ts @@ -78,7 +78,8 @@ export async function setCluster(clusterReq: ClusterRequest) { // @todo: needs documenting. export async function deleteCluster( - cluster: string + cluster: string, + removeKubeConfig?: boolean ): Promise<{ clusters: ConfigState['clusters'] }> { if (cluster) { const kubeconfig = await findKubeconfigByClusterName(cluster); @@ -89,12 +90,10 @@ export async function deleteCluster( } } - return request( - `/cluster/${cluster}`, - { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, - false, - false - ); + const url = `/cluster/${cluster}?removeKubeConfig=${removeKubeConfig}`; + + // need a way to add a param that passes 'removeKubeConfig' to go backend + return request(url, { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, false, false); } /**