From ec39a19beb8ef5a26b9fd52ff316eeba9849f4ea Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Sun, 8 Mar 2026 20:17:01 -0700 Subject: [PATCH] feat: add XDG_CACHE_HOME support to Go implementations Adds a new CacheDir() function that respects the XDG_CACHE_HOME environment variable, matching the bash scripts' behavior. kubectx and kubens now prefer XDG_CACHE_HOME when set, falling back to $HOME/.kube otherwise. This aligns the Go implementations with the bash scripts' cache directory logic. Includes comprehensive tests covering all scenarios: - XDG_CACHE_HOME set (returns the XDG value) - XDG_CACHE_HOME unset (falls back to $HOME/.kube) - Neither set (returns empty string) --- cmd/kubectx/state.go | 6 +++--- cmd/kubectx/state_test.go | 14 ++++++++++++++ cmd/kubens/statefile.go | 2 +- internal/cmdutil/util.go | 14 ++++++++++++++ internal/cmdutil/util_test.go | 27 +++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/cmd/kubectx/state.go b/cmd/kubectx/state.go index 90cb18c..ab3e2e3 100644 --- a/cmd/kubectx/state.go +++ b/cmd/kubectx/state.go @@ -24,11 +24,11 @@ import ( ) func kubectxPrevCtxFile() (string, error) { - home := cmdutil.HomeDir() - if home == "" { + dir := cmdutil.CacheDir() + if dir == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } - return filepath.Join(home, ".kube", "kubectx"), nil + return filepath.Join(dir, "kubectx"), nil } // readLastContext returns the saved previous context diff --git a/cmd/kubectx/state_test.go b/cmd/kubectx/state_test.go index 5a3e347..1647603 100644 --- a/cmd/kubectx/state_test.go +++ b/cmd/kubectx/state_test.go @@ -73,6 +73,7 @@ func Test_writeLastContext(t *testing.T) { func Test_kubectxFilePath(t *testing.T) { t.Setenv("HOME", filepath.FromSlash("/foo/bar")) + t.Setenv("XDG_CACHE_HOME", "") expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx") v, err := kubectxPrevCtxFile() @@ -84,6 +85,19 @@ func Test_kubectxFilePath(t *testing.T) { } } +func Test_kubectxFilePath_xdgCacheHome(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", filepath.FromSlash("/tmp/xdg-cache")) + + expected := filepath.Join(filepath.FromSlash("/tmp/xdg-cache"), "kubectx") + v, err := kubectxPrevCtxFile() + if err != nil { + t.Fatal(err) + } + if v != expected { + t.Fatalf("expected=\"%s\" got=\"%s\"", expected, v) + } +} + func Test_kubectxFilePath_error(t *testing.T) { t.Setenv("HOME", "") t.Setenv("USERPROFILE", "") diff --git a/cmd/kubens/statefile.go b/cmd/kubens/statefile.go index 03a5c3a..d6dd1e1 100644 --- a/cmd/kubens/statefile.go +++ b/cmd/kubens/statefile.go @@ -24,7 +24,7 @@ import ( "github.com/ahmetb/kubectx/internal/cmdutil" ) -var defaultDir = filepath.Join(cmdutil.HomeDir(), ".kube", "kubens") +var defaultDir = filepath.Join(cmdutil.CacheDir(), "kubens") type NSFile struct { dir string diff --git a/internal/cmdutil/util.go b/internal/cmdutil/util.go index df68c3f..a1395d9 100644 --- a/internal/cmdutil/util.go +++ b/internal/cmdutil/util.go @@ -17,6 +17,7 @@ package cmdutil import ( "errors" "os" + "path/filepath" ) func HomeDir() string { @@ -27,6 +28,19 @@ func HomeDir() string { return home } +// CacheDir returns XDG_CACHE_HOME if set, otherwise $HOME/.kube, +// matching the bash scripts' behavior: ${XDG_CACHE_HOME:-$HOME/.kube}. +func CacheDir() string { + if xdg := os.Getenv("XDG_CACHE_HOME"); xdg != "" { + return xdg + } + home := HomeDir() + if home == "" { + return "" + } + return filepath.Join(home, ".kube") +} + // IsNotFoundErr determines if the underlying error is os.IsNotExist. func IsNotFoundErr(err error) bool { for e := err; e != nil; e = errors.Unwrap(e) { diff --git a/internal/cmdutil/util_test.go b/internal/cmdutil/util_test.go index bf2373c..78ceb32 100644 --- a/internal/cmdutil/util_test.go +++ b/internal/cmdutil/util_test.go @@ -15,6 +15,7 @@ package cmdutil import ( + "path/filepath" "testing" "github.com/ahmetb/kubectx/internal/testutil" @@ -78,3 +79,29 @@ func Test_homeDir(t *testing.T) { }) } } + +func TestCacheDir(t *testing.T) { + t.Run("XDG_CACHE_HOME set", func(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", "/tmp/xdg-cache") + t.Setenv("HOME", "/home/user") + if got := CacheDir(); got != "/tmp/xdg-cache" { + t.Errorf("expected:%q got:%q", "/tmp/xdg-cache", got) + } + }) + t.Run("XDG_CACHE_HOME unset, falls back to HOME/.kube", func(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", "") + t.Setenv("HOME", "/home/user") + want := filepath.Join("/home/user", ".kube") + if got := CacheDir(); got != want { + t.Errorf("expected:%q got:%q", want, got) + } + }) + t.Run("neither set", func(t *testing.T) { + t.Setenv("XDG_CACHE_HOME", "") + t.Setenv("HOME", "") + t.Setenv("USERPROFILE", "") + if got := CacheDir(); got != "" { + t.Errorf("expected:%q got:%q", "", got) + } + }) +}