Skip to content

Commit 9d3e8cd

Browse files
committed
app: home: Add delete button for clusters
Signed-off-by: Vincent T <[email protected]>
1 parent d24fc9f commit 9d3e8cd

File tree

4 files changed

+68
-33
lines changed

4 files changed

+68
-33
lines changed

backend/cmd/headlamp.go

+32-2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,17 @@ func serveWithNoCacheHeader(fs http.Handler) http.HandlerFunc {
234234
}
235235
}
236236

237+
// defaultKubeConfigFile returns the default path to the kubeconfig file.
238+
func defaultKubeConfigFile() (string, error) {
239+
homeDir, err := os.UserHomeDir()
240+
if err != nil {
241+
return "", fmt.Errorf("failed to get user home directory: %v", err)
242+
}
243+
244+
kubeConfigFile := filepath.Join(homeDir, ".kube", "config")
245+
return kubeConfigFile, nil
246+
}
247+
237248
// defaultKubeConfigPersistenceDir returns the default directory to store kubeconfig
238249
// files of clusters that are loaded in Headlamp.
239250
func defaultKubeConfigPersistenceDir() (string, error) {
@@ -1384,6 +1395,26 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
13841395
return
13851396
}
13861397

1398+
removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true"
1399+
1400+
if removeKubeConfig {
1401+
// delete context from actual deafult kubecofig file
1402+
kubeConfigFile, err := defaultKubeConfigFile()
1403+
if err != nil {
1404+
logger.Log(logger.LevelError, map[string]string{"cluster": name}, err, "failed to get default kubeconfig file path")
1405+
http.Error(w, "failed to get default kubeconfig file path", http.StatusInternalServerError)
1406+
return
1407+
}
1408+
1409+
// Use kubeConfigFile to remove the context from the default kubeconfig file
1410+
err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile)
1411+
if err != nil {
1412+
logger.Log(logger.LevelError, map[string]string{"cluster": name}, err, "removing context from default kubeconfig file")
1413+
http.Error(w, "removing context from default kubeconfig file", http.StatusInternalServerError)
1414+
return
1415+
}
1416+
}
1417+
13871418
kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile()
13881419
if err != nil {
13891420
logger.Log(logger.LevelError, map[string]string{"cluster": name},
@@ -1396,8 +1427,7 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
13961427
logger.Log(logger.LevelInfo, map[string]string{
13971428
"cluster": name,
13981429
"kubeConfigPersistenceFile": kubeConfigPersistenceFile,
1399-
},
1400-
nil, "Removing cluster from kubeconfig")
1430+
}, nil, "Removing cluster from kubeconfig")
14011431

14021432
err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile)
14031433
if err != nil {

frontend/src/components/App/Home/index.tsx

+29-23
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ import { ConfirmDialog } from '../../common';
2525
import ResourceTable from '../../common/Resource/ResourceTable';
2626
import RecentClusters from './RecentClusters';
2727

28+
/**
29+
* Gets the origin of a cluster.
30+
*
31+
* @param cluster
32+
* @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file.
33+
*/
34+
function getOrigin(cluster: Cluster): string {
35+
if (cluster.meta_data?.source === 'kubeconfig') {
36+
const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config';
37+
return `Kubeconfig: ${kubeconfigPath}`;
38+
} else if (cluster.meta_data?.source === 'dynamic_cluster') {
39+
return t('translation|Plugin');
40+
} else if (cluster.meta_data?.source === 'in_cluster') {
41+
return t('translation|In-cluster');
42+
}
43+
return 'Unknown';
44+
}
45+
2846
function ContextMenu({ cluster }: { cluster: Cluster }) {
2947
const { t } = useTranslation(['translation']);
3048
const history = useHistory();
@@ -33,8 +51,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
3351
const menuId = useId('context-menu');
3452
const [openConfirmDialog, setOpenConfirmDialog] = React.useState(false);
3553

36-
function removeCluster(cluster: Cluster) {
37-
deleteCluster(cluster.name || '')
54+
function removeCluster(cluster: Cluster, removeKubeconfig?: boolean) {
55+
deleteCluster(cluster.name || '', removeKubeconfig)
3856
.then(config => {
3957
dispatch(setConfig(config));
4058
})
@@ -92,7 +110,8 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
92110
>
93111
<ListItemText>{t('translation|Settings')}</ListItemText>
94112
</MenuItem>
95-
{helpers.isElectron() && cluster.meta_data?.source === 'dynamic_cluster' && (
113+
114+
{helpers.isElectron() && (
96115
<MenuItem
97116
onClick={() => {
98117
setOpenConfirmDialog(true);
@@ -109,13 +128,18 @@ function ContextMenu({ cluster }: { cluster: Cluster }) {
109128
handleClose={() => setOpenConfirmDialog(false)}
110129
onConfirm={() => {
111130
setOpenConfirmDialog(false);
112-
removeCluster(cluster);
131+
if (cluster.meta_data?.source !== 'dynamic_cluster') {
132+
removeCluster(cluster, true);
133+
} else {
134+
removeCluster(cluster);
135+
}
113136
}}
114137
title={t('translation|Delete Cluster')}
115138
description={t(
116-
'translation|Are you sure you want to remove the cluster "{{ clusterName }}"?',
139+
'translation|This action will delete cluster "{{ clusterName }}" from {{ source }}.',
117140
{
118141
clusterName: cluster.name,
142+
source: getOrigin(cluster),
119143
}
120144
)}
121145
/>
@@ -239,24 +263,6 @@ function HomeComponent(props: HomeComponentProps) {
239263
.sort();
240264
}
241265

242-
/**
243-
* Gets the origin of a cluster.
244-
*
245-
* @param cluster
246-
* @returns A description of where the cluster is picked up from: dynamic, in-cluster, or from a kubeconfig file.
247-
*/
248-
function getOrigin(cluster: Cluster): string {
249-
if (cluster.meta_data?.source === 'kubeconfig') {
250-
const kubeconfigPath = process.env.KUBECONFIG ?? '~/.kube/config';
251-
return `Kubeconfig: ${kubeconfigPath}`;
252-
} else if (cluster.meta_data?.source === 'dynamic_cluster') {
253-
return t('translation|Plugin');
254-
} else if (cluster.meta_data?.source === 'in_cluster') {
255-
return t('translation|In-cluster');
256-
}
257-
return 'Unknown';
258-
}
259-
260266
const memoizedComponent = React.useMemo(
261267
() => (
262268
<PageGrid>

frontend/src/components/common/ConfirmDialog.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { DialogTitle } from './Dialog';
99

1010
export interface ConfirmDialogProps extends MuiDialogProps {
1111
title: string;
12-
description: string;
12+
description: string | JSX.Element;
1313
onConfirm: () => void;
1414
handleClose: () => void;
1515
}

frontend/src/lib/k8s/api/v1/clusterApi.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ export async function setCluster(clusterReq: ClusterRequest) {
7878
// @todo: needs documenting.
7979

8080
export async function deleteCluster(
81-
cluster: string
81+
cluster: string,
82+
removeKubeConfig?: boolean
8283
): Promise<{ clusters: ConfigState['clusters'] }> {
8384
if (cluster) {
8485
const kubeconfig = await findKubeconfigByClusterName(cluster);
@@ -89,12 +90,10 @@ export async function deleteCluster(
8990
}
9091
}
9192

92-
return request(
93-
`/cluster/${cluster}`,
94-
{ method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } },
95-
false,
96-
false
97-
);
93+
const url = `/cluster/${cluster}?removeKubeConfig=${removeKubeConfig}`;
94+
95+
// need a way to add a param that passes 'removeKubeConfig' to go backend
96+
return request(url, { method: 'DELETE', headers: { ...getHeadlampAPIHeaders() } }, false, false);
9897
}
9998

10099
/**

0 commit comments

Comments
 (0)