diff --git a/backend/cmd/headlamp.go b/backend/cmd/headlamp.go index fd4bd3dd81..9a11b09142 100644 --- a/backend/cmd/headlamp.go +++ b/backend/cmd/headlamp.go @@ -234,6 +234,18 @@ 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) { @@ -1365,6 +1377,35 @@ func (c *HeadlampConfig) addContextsToStore(contexts []kubeconfig.Context, setup return setupErrors } +// removeContextFromFile removes the context from the kubeconfig file. +func removeContextFromFile(w http.ResponseWriter, contextName string) error { + kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile() + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, + err, "getting default kubeconfig persistence file") + http.Error(w, "getting default kubeconfig persistence file", http.StatusInternalServerError) + + return err + } + + logger.Log(logger.LevelInfo, map[string]string{ + "cluster": contextName, + "kubeConfigPersistenceFile": kubeConfigPersistenceFile, + }, + nil, "Removing cluster from kubeconfig") + + err = kubeconfig.RemoveContextFromFile(contextName, kubeConfigPersistenceFile) + if err != nil { + logger.Log(logger.LevelError, map[string]string{"cluster": contextName}, + err, "removing cluster from kubeconfig") + http.Error(w, "removing cluster from kubeconfig", http.StatusInternalServerError) + + return err + } + + return nil +} + // deleteCluster deletes the cluster from the store and updates the kubeconfig file. func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { if err := checkHeadlampBackendToken(w, r); err != nil { @@ -1384,28 +1425,32 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) { return } - kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile() - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": name}, - err, "getting default kubeconfig persistence file") - http.Error(w, "getting default kubeconfig persistence file", http.StatusInternalServerError) + removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true" - return - } + if removeKubeConfig { + // delete context from actual default 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) - logger.Log(logger.LevelInfo, map[string]string{ - "cluster": name, - "kubeConfigPersistenceFile": kubeConfigPersistenceFile, - }, - nil, "Removing cluster from kubeconfig") + return + } - err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile) - if err != nil { - logger.Log(logger.LevelError, map[string]string{"cluster": name}, - err, "removing cluster from kubeconfig") - http.Error(w, "removing cluster from kubeconfig", http.StatusInternalServerError) + // 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 + return + } + } else { + if err := removeContextFromFile(w, name); err != nil { + return + } } logger.Log(logger.LevelInfo, map[string]string{"cluster": name, "proxy": name}, diff --git a/frontend/src/components/App/Home/index.tsx b/frontend/src/components/App/Home/index.tsx index abed5454a9..198c318444 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, t: any): 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)); }) @@ -91,7 +109,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { > {t('translation|Settings')} - {helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && ( + + {helpers.isElectron() && ( { setOpenConfirmDialog(true); @@ -105,18 +124,28 @@ function ContextMenu({ cluster }: { cluster: Cluster }) { setOpenConfirmDialog(false)} + 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, t), } )} + checkboxDescription={ + cluster.meta_data?.source !== 'dynamic_cluster' ? t('Delete from kubeconfig') : '' + } /> ); @@ -238,24 +267,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( () => ( @@ -286,10 +297,8 @@ function HomeComponent(props: HomeComponentProps) { }, { label: t('Origin'), - getValue: cluster => getOrigin(cluster), - render: ({ name }) => ( - {getOrigin(clusters[name])} - ), + getValue: cluster => getOrigin(cluster, t), + render: cluster => {getOrigin(cluster, t)}, }, { label: t('Status'), diff --git a/frontend/src/components/common/ConfirmDialog.tsx b/frontend/src/components/common/ConfirmDialog.tsx index 3ba49b0571..c8abbe073a 100644 --- a/frontend/src/components/common/ConfirmDialog.tsx +++ b/frontend/src/components/common/ConfirmDialog.tsx @@ -1,15 +1,18 @@ +import { Checkbox } from '@mui/material'; +import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import MuiDialog, { DialogProps as MuiDialogProps } from '@mui/material/Dialog'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; -import React, { ReactNode } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { DialogTitle } from './Dialog'; export interface ConfirmDialogProps extends MuiDialogProps { title: string; - description: ReactNode; + description: string | React.ReactNode; + checkboxDescription?: string; onConfirm: () => void; handleClose: () => void; } @@ -17,12 +20,22 @@ export interface ConfirmDialogProps extends MuiDialogProps { export function ConfirmDialog(props: ConfirmDialogProps) { const { onConfirm, open, handleClose, title, description } = props; const { t } = useTranslation(); + const [checkedChoice, setcheckedChoice] = React.useState(false); function onConfirmationClicked() { handleClose(); onConfirm(); } + function closeDialog() { + setcheckedChoice(false); + handleClose(); + } + + function handleChoiceToggle() { + setcheckedChoice(!checkedChoice); + } + const focusedRef = React.useCallback((node: HTMLElement) => { if (node !== null) { node.setAttribute('tabindex', '-1'); @@ -34,21 +47,46 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
{title} {description} + {props.checkboxDescription && ( + + + {props.checkboxDescription} + + + + )} - - + {props.checkboxDescription ? ( + + ) : ( + + )}
diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index 025012a3ce..b5e3d687a2 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -9,15 +9,16 @@ "Cancel": "Abbrechen", "Authenticate": "Authentifizieren Sie", "Error authenticating": "Fehler beim Authentifizieren", + "Plugin": "", + "In-cluster": "", "Actions": "Aktionen", "View": "Ansicht", "Settings": "Einstellungen", "Delete": "Löschen", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Sind Sie sicher, dass Sie den Cluster \"{{ clusterName }}\" entfernen möchten?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Aktiv", - "Plugin": "", - "In-cluster": "", "Home": "Startseite", "All Clusters": "Alle Cluster", "Name": "Name", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Cluster-Einstellungen ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Cluster entfernen", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Sind Sie sicher, dass Sie den Cluster \"{{ clusterName }}\" entfernen möchten?", "Server": "Server", "light theme": "helles Design", "dark theme": "dunkles Design", @@ -145,6 +147,7 @@ "Offline": "Offline", "Lost connection to the cluster.": "", "No": "Nein", + "I Agree": "", "Yes": "Ja", "Create {{ name }}": "", "Toggle fullscreen": "Vollbild ein/aus", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 7874af5189..e59c1c901f 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancel", "Authenticate": "Authenticate", "Error authenticating": "Error authenticating", + "Plugin": "Plugin", + "In-cluster": "In-cluster", "Actions": "Actions", "View": "View", "Settings": "Settings", "Delete": "Delete", "Delete Cluster": "Delete Cluster", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Are you sure you want to remove the cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.", + "Delete from kubeconfig": "Delete from kubeconfig", "Active": "Active", - "Plugin": "Plugin", - "In-cluster": "In-cluster", "Home": "Home", "All Clusters": "All Clusters", "Name": "Name", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Cluster Settings ({{ clusterName }})", "Go to cluster": "Go to cluster", "Remove Cluster": "Remove Cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Are you sure you want to remove the cluster \"{{ clusterName }}\"?", "Server": "Server", "light theme": "light theme", "dark theme": "dark theme", @@ -145,6 +147,7 @@ "Offline": "Offline", "Lost connection to the cluster.": "Lost connection to the cluster.", "No": "No", + "I Agree": "I Agree", "Yes": "Yes", "Create {{ name }}": "Create {{ name }}", "Toggle fullscreen": "Toggle fullscreen", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index a70637833b..eca647d939 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancelar", "Authenticate": "Autenticar", "Error authenticating": "Error al autenticarse", + "Plugin": "", + "In-cluster": "", "Actions": "Acciones", "View": "Ver", "Settings": "Definiciones", "Delete": "Borrar", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "¿Está seguro de que desea eliminar el cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Activo", - "Plugin": "", - "In-cluster": "", "Home": "Inicio", "All Clusters": "Todos los Clusters", "Name": "Nombre", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Configuración del cluster ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Eliminar cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "¿Está seguro de que desea eliminar el cluster \"{{ clusterName }}\"?", "Server": "Servidor", "light theme": "tema claro", "dark theme": "tema oscuro", @@ -145,6 +147,7 @@ "Offline": "Desconectado", "Lost connection to the cluster.": "", "No": "No", + "I Agree": "", "Yes": "Sí", "Create {{ name }}": "", "Toggle fullscreen": "Alternar pantalla completa", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index 61134299e9..3f0cf19cae 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancel", "Authenticate": "Authentifier", "Error authenticating": "Erreur d'authentification", + "Plugin": "", + "In-cluster": "", "Actions": "Actions", "View": "Vue", "Settings": "Paramètres", "Delete": "Supprimer", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Voulez-vous vraiment supprimer le cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Actif", - "Plugin": "", - "In-cluster": "", "Home": "Accueil", "All Clusters": "Tous les clusters", "Name": "Nom", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Paramètres du cluster ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Supprimer le cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Voulez-vous vraiment supprimer le cluster \"{{ clusterName }}\"?", "Server": "Serveur", "light theme": "thème clair", "dark theme": "thème sombre", @@ -145,6 +147,7 @@ "Offline": "Hors ligne", "Lost connection to the cluster.": "", "No": "Non", + "I Agree": "", "Yes": "Oui", "Create {{ name }}": "", "Toggle fullscreen": "Basculer en mode plein écran", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 47caf5a133..1af9afbb9b 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -9,15 +9,16 @@ "Cancel": "Cancelar", "Authenticate": "Autenticar", "Error authenticating": "Erro ao autenticar", + "Plugin": "", + "In-cluster": "", "Actions": "Acções", "View": "Ver", "Settings": "Definições", "Delete": "Apagar", "Delete Cluster": "", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Tem a certeza que quer remover o cluster \"{{ clusterName }}\"?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "Activo", - "Plugin": "", - "In-cluster": "", "Home": "Início", "All Clusters": "Todos os Clusters", "Name": "Nome", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "Definições do cluster ({{ clusterName }})", "Go to cluster": "", "Remove Cluster": "Remover Cluster", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "Tem a certeza que quer remover o cluster \"{{ clusterName }}\"?", "Server": "Servidor", "light theme": "tema claro", "dark theme": "tema escuro", @@ -145,6 +147,7 @@ "Offline": "Desconectado", "Lost connection to the cluster.": "", "No": "Não", + "I Agree": "", "Yes": "Sim", "Create {{ name }}": "", "Toggle fullscreen": "Alternar ecrã inteiro", diff --git a/frontend/src/i18n/locales/zh-tw/translation.json b/frontend/src/i18n/locales/zh-tw/translation.json index f97c5dee99..d897be5b1a 100644 --- a/frontend/src/i18n/locales/zh-tw/translation.json +++ b/frontend/src/i18n/locales/zh-tw/translation.json @@ -9,15 +9,16 @@ "Cancel": "取消", "Authenticate": "驗證", "Error authenticating": "驗證錯誤", + "Plugin": "插件", + "In-cluster": "叢集內", "Actions": "操作", "View": "查看", "Settings": "設置", "Delete": "刪除", "Delete Cluster": "刪除叢集", - "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "您確定要移除叢集 \"{{ clusterName }}\" 嗎?", + "This action will delete cluster \"{{ clusterName }}\" from {{ source }}.": "", + "Delete from kubeconfig": "", "Active": "活躍", - "Plugin": "外掛", - "In-cluster": "叢集內", "Home": "首頁", "All Clusters": "所有叢集", "Name": "名稱", @@ -81,6 +82,7 @@ "Cluster Settings ({{ clusterName }})": "叢集設置 ({{ clusterName }})", "Go to cluster": "前往叢集", "Remove Cluster": "移除叢集", + "Are you sure you want to remove the cluster \"{{ clusterName }}\"?": "您確定要移除叢集 \"{{ clusterName }}\" 嗎?", "Server": "伺服器", "light theme": "淺色主題", "dark theme": "深色主題", @@ -145,6 +147,7 @@ "Offline": "離線", "Lost connection to the cluster.": "與叢集的連線遺失。", "No": "否", + "I Agree": "", "Yes": "是", "Create {{ name }}": "新增 {{ name }}", "Toggle fullscreen": "切換全螢幕", diff --git a/frontend/src/lib/k8s/api/v1/clusterApi.ts b/frontend/src/lib/k8s/api/v1/clusterApi.ts index 374a2d59bd..4a13668108 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,11 @@ export async function deleteCluster( } } - return request( - `/cluster/${cluster}`, - { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, - false, - false - ); + const url = removeKubeConfig + ? `/cluster/${cluster}?removeKubeConfig=true` + : `/cluster/${cluster}`; + + return request(url, { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, false, false); } /**