@@ -11,6 +11,7 @@ import (
11
11
"runtime"
12
12
"strings"
13
13
14
+ "github.com/headlamp-k8s/headlamp/backend/pkg/kubeconfig"
14
15
"github.com/headlamp-k8s/headlamp/backend/pkg/logger"
15
16
"github.com/knadh/koanf"
16
17
"github.com/knadh/koanf/providers/basicflag"
@@ -217,3 +218,175 @@ func GetDefaultKubeConfigPath() string {
217
218
218
219
return filepath .Join (homeDirectory , ".kube" , "config" )
219
220
}
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
+ }
0 commit comments