Skip to content

Commit b1145b8

Browse files
committed
app: home: Add delete button for non dynamic clusters
(waiting for merge of improve meta data branch) Signed-off-by: Vincent T <[email protected]>
1 parent 92147eb commit b1145b8

16 files changed

+549
-113
lines changed

backend/cmd/headlamp.go

+34-48
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/gorilla/handlers"
2828
"github.com/gorilla/mux"
2929
"github.com/headlamp-k8s/headlamp/backend/pkg/cache"
30+
"github.com/headlamp-k8s/headlamp/backend/pkg/config"
3031
"github.com/headlamp-k8s/headlamp/backend/pkg/helm"
3132
"github.com/headlamp-k8s/headlamp/backend/pkg/kubeconfig"
3233
"github.com/headlamp-k8s/headlamp/backend/pkg/logger"
@@ -245,40 +246,11 @@ func serveWithNoCacheHeader(fs http.Handler) http.HandlerFunc {
245246
// defaultKubeConfigPersistenceDir returns the default directory to store kubeconfig
246247
// files of clusters that are loaded in Headlamp.
247248
func defaultKubeConfigPersistenceDir() (string, error) {
248-
userConfigDir, err := os.UserConfigDir()
249-
if err == nil {
250-
kubeConfigDir := filepath.Join(userConfigDir, "Headlamp", "kubeconfigs")
251-
if isWindows {
252-
// golang is wrong for config folder on windows.
253-
// This matches env-paths and headlamp-plugin.
254-
kubeConfigDir = filepath.Join(userConfigDir, "Headlamp", "Config", "kubeconfigs")
255-
}
256-
257-
// Create the directory if it doesn't exist.
258-
fileMode := 0o755
259-
260-
err = os.MkdirAll(kubeConfigDir, fs.FileMode(fileMode))
261-
if err == nil {
262-
return kubeConfigDir, nil
263-
}
264-
}
265-
266-
// if any error occurred, fallback to the current directory.
267-
ex, err := os.Executable()
268-
if err == nil {
269-
return filepath.Dir(ex), nil
270-
}
271-
272-
return "", fmt.Errorf("failed to get default kubeconfig persistence directory: %v", err)
249+
return config.DefaultKubeConfigPersistenceDir()
273250
}
274251

275252
func defaultKubeConfigPersistenceFile() (string, error) {
276-
kubeConfigDir, err := defaultKubeConfigPersistenceDir()
277-
if err != nil {
278-
return "", err
279-
}
280-
281-
return filepath.Join(kubeConfigDir, "config"), nil
253+
return config.DefaultKubeConfigPersistenceFile()
282254
}
283255

284256
// addPluginRoutes adds plugin routes to a router.
@@ -1467,28 +1439,42 @@ func (c *HeadlampConfig) deleteCluster(w http.ResponseWriter, r *http.Request) {
14671439
return
14681440
}
14691441

1470-
kubeConfigPersistenceFile, err := defaultKubeConfigPersistenceFile()
1471-
if err != nil {
1472-
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1473-
err, "getting default kubeconfig persistence file")
1474-
http.Error(w, "getting default kubeconfig persistence file", http.StatusInternalServerError)
1442+
removeKubeConfig := r.URL.Query().Get("removeKubeConfig") == "true"
14751443

1476-
return
1444+
if removeKubeConfig {
1445+
kubeConfigFile := config.GetDefaultKubeConfigPath()
1446+
1447+
if err != nil {
1448+
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1449+
err, "failed to get default kubeconfig file path")
1450+
http.Error(w, "failed to get default kubeconfig file path", http.StatusInternalServerError)
1451+
1452+
return
1453+
}
1454+
1455+
err = kubeconfig.RemoveContextFromFile(name, kubeConfigFile)
1456+
if err != nil {
1457+
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1458+
err, "removing context from default kubeconfig file")
1459+
http.Error(w, "removing context from default kubeconfig file", http.StatusInternalServerError)
1460+
1461+
return
1462+
}
14771463
}
14781464

1479-
logger.Log(logger.LevelInfo, map[string]string{
1480-
"cluster": name,
1481-
"kubeConfigPersistenceFile": kubeConfigPersistenceFile,
1482-
},
1483-
nil, "Removing cluster from kubeconfig")
1465+
if !removeKubeConfig {
1466+
configPathsList, pathErr := config.CollectMultiConfigPaths()
1467+
if pathErr != nil {
1468+
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1469+
pathErr, "collecting multi config paths")
1470+
http.Error(w, "collecting multi config paths", http.StatusInternalServerError)
14841471

1485-
err = kubeconfig.RemoveContextFromFile(name, kubeConfigPersistenceFile)
1486-
if err != nil {
1487-
logger.Log(logger.LevelError, map[string]string{"cluster": name},
1488-
err, "removing cluster from kubeconfig")
1489-
http.Error(w, "removing cluster from kubeconfig", http.StatusInternalServerError)
1472+
return
1473+
}
14901474

1491-
return
1475+
if err := config.RemoveContextFromDefaultKubeConfig(name, configPathsList...); err != nil {
1476+
return
1477+
}
14921478
}
14931479

14941480
logger.Log(logger.LevelInfo, map[string]string{"cluster": name, "proxy": name},

backend/pkg/config/config.go

+173
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212
"strings"
1313

14+
"github.com/headlamp-k8s/headlamp/backend/pkg/kubeconfig"
1415
"github.com/headlamp-k8s/headlamp/backend/pkg/logger"
1516
"github.com/knadh/koanf"
1617
"github.com/knadh/koanf/providers/basicflag"
@@ -217,3 +218,175 @@ func GetDefaultKubeConfigPath() string {
217218

218219
return filepath.Join(homeDirectory, ".kube", "config")
219220
}
221+
222+
// DefaultKubeConfigPersistenceDir returns the default directory to store kubeconfig
223+
// files of clusters that are loaded in Headlamp.
224+
func DefaultKubeConfigPersistenceDir() (string, error) {
225+
userConfigDir, err := os.UserConfigDir()
226+
227+
if err == nil {
228+
kubeConfigDir := filepath.Join(userConfigDir, "Headlamp", "kubeconfigs")
229+
if runtime.GOOS == "windows" {
230+
// golang is wrong for config folder on windows.
231+
// This matches env-paths and headlamp-plugin.
232+
kubeConfigDir = filepath.Join(userConfigDir, "Headlamp", "Config", "kubeconfigs")
233+
}
234+
235+
// Create the directory if it doesn't exist.
236+
fileMode := 0o755
237+
238+
err = os.MkdirAll(kubeConfigDir, fs.FileMode(fileMode))
239+
if err == nil {
240+
return kubeConfigDir, nil
241+
}
242+
}
243+
244+
// if any error occurred, fallback to the current directory.
245+
ex, err := os.Executable()
246+
if err == nil {
247+
return filepath.Dir(ex), nil
248+
}
249+
250+
return "", fmt.Errorf("failed to get default kubeconfig persistence directory: %v", err)
251+
}
252+
253+
func DefaultKubeConfigPersistenceFile() (string, error) {
254+
kubeConfigDir, err := DefaultKubeConfigPersistenceDir()
255+
if err != nil {
256+
return "", err
257+
}
258+
259+
return filepath.Join(kubeConfigDir, "config"), nil
260+
}
261+
262+
// collectMultiConfigPaths looks at the default dynamic directory
263+
// (e.g. ~/.config/Headlamp/kubeconfigs) and returns any files found there.
264+
// This is called from the 'else' block in deleteCluster().
265+
//
266+
//nolint:prealloc
267+
func CollectMultiConfigPaths() ([]string, error) {
268+
dynamicDir, err := DefaultKubeConfigPersistenceDir()
269+
if err != nil {
270+
return nil, fmt.Errorf("getting default kubeconfig persistence dir: %w", err)
271+
}
272+
273+
entries, err := os.ReadDir(dynamicDir)
274+
if err != nil {
275+
return nil, fmt.Errorf("reading dynamic kubeconfig directory: %w", err)
276+
}
277+
278+
var configPaths []string
279+
280+
for _, entry := range entries {
281+
// Optionally skip directories or non-kubeconfig files, if needed.
282+
if entry.IsDir() {
283+
continue
284+
}
285+
286+
// Validate known kubeconfig file extensions
287+
if !strings.HasSuffix(entry.Name(), ".yaml") && !strings.HasSuffix(entry.Name(), ".yml") {
288+
continue
289+
}
290+
291+
filePath := filepath.Join(dynamicDir, entry.Name())
292+
293+
configPaths = append(configPaths, filePath)
294+
}
295+
296+
return configPaths, nil
297+
}
298+
299+
// RemoveContextFromConfigs does the real iteration over the configPaths.
300+
func RemoveContextFromConfigs(contextName string, configPaths []string) error {
301+
var removed bool
302+
303+
for _, filePath := range configPaths {
304+
logger.Log(
305+
logger.LevelInfo,
306+
map[string]string{
307+
"cluster": contextName,
308+
"kubeConfigPersistenceFile": filePath,
309+
},
310+
nil,
311+
"Trying to remove context from kubeconfig",
312+
)
313+
314+
err := kubeconfig.RemoveContextFromFile(contextName, filePath)
315+
if err == nil {
316+
removed = true
317+
318+
logger.Log(logger.LevelInfo,
319+
map[string]string{"cluster": contextName, "file": filePath},
320+
nil, "Removed context from kubeconfig",
321+
)
322+
323+
break
324+
}
325+
326+
if strings.Contains(err.Error(), "context not found") {
327+
logger.Log(logger.LevelInfo,
328+
map[string]string{"cluster": contextName, "file": filePath},
329+
nil, "Context not in this file; checking next.",
330+
)
331+
332+
continue
333+
}
334+
335+
logger.Log(logger.LevelError,
336+
map[string]string{"cluster": contextName},
337+
err, "removing cluster from kubeconfig",
338+
)
339+
340+
return err
341+
}
342+
343+
if !removed {
344+
e := fmt.Errorf("context %q not found in any provided kubeconfig file(s)", contextName)
345+
346+
logger.Log(
347+
logger.LevelError,
348+
map[string]string{"cluster": contextName},
349+
e,
350+
"context not found in any file",
351+
)
352+
353+
return e
354+
}
355+
356+
return nil
357+
}
358+
359+
func RemoveContextFromDefaultKubeConfig(
360+
contextName string,
361+
configPaths ...string,
362+
) error {
363+
// Check if contextName is empty
364+
if contextName == "" {
365+
return fmt.Errorf("context name cannot be empty")
366+
}
367+
368+
// If no specific paths passed, fallback to the default.
369+
if len(configPaths) == 0 {
370+
discoveredPath, err := DefaultKubeConfigPersistenceFile()
371+
if err != nil {
372+
logger.Log(
373+
logger.LevelError,
374+
map[string]string{"cluster": contextName},
375+
err,
376+
"getting default kubeconfig persistence file",
377+
)
378+
379+
return fmt.Errorf("getting default kubeconfig persistence file: %w", err)
380+
}
381+
382+
configPaths = []string{discoveredPath}
383+
}
384+
385+
// Check if configPaths is empty
386+
if len(configPaths) == 0 {
387+
return fmt.Errorf("no config paths provided")
388+
}
389+
390+
// Hand off to a small helper function that handles multi-file iteration.
391+
return RemoveContextFromConfigs(contextName, configPaths)
392+
}

backend/pkg/kubeconfig/kubeconfig_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"os"
9+
"path/filepath"
910
"testing"
1011

1112
"github.com/headlamp-k8s/headlamp/backend/pkg/config"
@@ -512,3 +513,28 @@ func TestHandleConfigLoadError(t *testing.T) {
512513
})
513514
}
514515
}
516+
517+
func TestRemoveContextFromDefaultKubeConfig(t *testing.T) {
518+
// 1) Create a temp directory for our test kubeconfig
519+
tmpDir := t.TempDir()
520+
mockConfigFile := filepath.Join(tmpDir, "config")
521+
522+
// 2) Copy "kubeconfig_remove" (which includes 'kubedelta') into that file
523+
testDataPath := filepath.Join("test_data", "kubeconfig_remove")
524+
testData, err := os.ReadFile(testDataPath)
525+
require.NoError(t, err, "failed to read test data for 'kubeconfig_remove'")
526+
527+
err = os.WriteFile(mockConfigFile, testData, 0o600)
528+
require.NoError(t, err, "failed to write test kubeconfig")
529+
530+
// 4) Call removeContextFromDefaultKubeConfig with our mock path as the third param
531+
err = config.RemoveContextFromDefaultKubeConfig("random-cluster-x", mockConfigFile)
532+
require.NoError(t, err, "removeContextFromDefaultKubeConfig should succeed")
533+
534+
// 5) Verify 'random-cluster-x' is removed from the file
535+
updatedData, err := os.ReadFile(mockConfigFile)
536+
require.NoError(t, err, "failed to read updated kubeconfig")
537+
538+
require.NotContains(t, string(updatedData), "random-cluster-x",
539+
"Expected 'random-cluster-x' context to be removed from kubeconfig")
540+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
apiVersion: v1
2+
clusters:
3+
- cluster:
4+
certificate-authority-data: dGVzdC1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YQ==
5+
server: https://kubernetes.docker.internal:6443
6+
name: random-cluster-x
7+
- cluster:
8+
certificate-authority-data: dGVzdC1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YQ==
9+
extensions:
10+
- extension:
11+
last-update: Mon, 26 Dec 2022 20:33:03 IST
12+
provider: random-cluster-y.sigs.k8s.io
13+
version: v1.28.0
14+
name: cluster_info
15+
server: https://127.0.0.1:60279
16+
name: random-cluster-y
17+
contexts:
18+
- context:
19+
cluster: random-cluster-x
20+
user: random-cluster-x
21+
name: random-cluster-x
22+
- context:
23+
cluster: random-cluster-y
24+
extensions:
25+
- extension:
26+
last-update: Mon, 26 Dec 2022 20:33:03 IST
27+
provider: random-cluster-y.sigs.k8s.io
28+
version: v1.28.0
29+
name: context_info
30+
namespace: default
31+
user: random-cluster-y
32+
name: random-cluster-y
33+
current-context: random-cluster-y
34+
kind: Config
35+
preferences: {}
36+
users:
37+
- name: random-cluster-x
38+
user:
39+
client-certificate-data: dGVzdC1jbGllbnQtY2VydGlmaWNhdGUtZGF0YQ==
40+
client-key-data: dGVzdC1jbGllbnQta2V5LWRhdGE=
41+
- name: random-cluster-y
42+
user:
43+
client-certificate-data: dGVzdC1jbGllbnQtY2VydGlmaWNhdGUtZGF0YQ==
44+
client-key-data: dGVzdC1jbGllbnQta2V5LWRhdGE=

0 commit comments

Comments
 (0)