diff --git a/cmd/kubectx/current.go b/cmd/kubectx/current.go index c8f07159..9a070098 100644 --- a/cmd/kubectx/current.go +++ b/cmd/kubectx/current.go @@ -6,7 +6,6 @@ import ( "github.com/pkg/errors" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" ) @@ -14,7 +13,7 @@ import ( type CurrentOp struct{} func (_op CurrentOp) Run(stdout, _ io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubectx/delete.go b/cmd/kubectx/delete.go index 6ed8893c..1ecf87e5 100644 --- a/cmd/kubectx/delete.go +++ b/cmd/kubectx/delete.go @@ -5,7 +5,6 @@ import ( "github.com/pkg/errors" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -36,7 +35,7 @@ func (op DeleteOp) Run(_, stderr io.Writer) error { // deleteContext deletes a context entry by NAME or current-context // indicated by ".". func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return deleteName, false, errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubectx/fzf.go b/cmd/kubectx/fzf.go index eefb398a..38fb5b0d 100644 --- a/cmd/kubectx/fzf.go +++ b/cmd/kubectx/fzf.go @@ -22,7 +22,7 @@ type InteractiveSwitchOp struct { func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { // parse kubeconfig just to see if it can be loaded - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") diff --git a/cmd/kubectx/list.go b/cmd/kubectx/list.go index 1a8976d1..783fd736 100644 --- a/cmd/kubectx/list.go +++ b/cmd/kubectx/list.go @@ -16,7 +16,7 @@ import ( type ListOp struct{} func (_ ListOp) Run(stdout, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { diff --git a/cmd/kubectx/rename.go b/cmd/kubectx/rename.go index 4ed99b4c..2ae0c702 100644 --- a/cmd/kubectx/rename.go +++ b/cmd/kubectx/rename.go @@ -6,7 +6,6 @@ import ( "github.com/pkg/errors" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -35,7 +34,7 @@ func parseRenameSyntax(v string) (string, string, bool) { // to the "new" value. If the old refers to the current-context, // current-context preference is also updated. func (op RenameOp) Run(_, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubectx/switch.go b/cmd/kubectx/switch.go index af40d6e3..f64b2843 100644 --- a/cmd/kubectx/switch.go +++ b/cmd/kubectx/switch.go @@ -5,7 +5,6 @@ import ( "github.com/pkg/errors" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -37,7 +36,7 @@ func switchContext(name string) (string, error) { return "", errors.Wrap(err, "failed to determine state file") } - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return "", errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubectx/unset.go b/cmd/kubectx/unset.go index df433bf9..243c1764 100644 --- a/cmd/kubectx/unset.go +++ b/cmd/kubectx/unset.go @@ -5,7 +5,6 @@ import ( "github.com/pkg/errors" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -14,7 +13,7 @@ import ( type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubens/current.go b/cmd/kubens/current.go index 9f051317..3ab42523 100644 --- a/cmd/kubens/current.go +++ b/cmd/kubens/current.go @@ -6,14 +6,13 @@ import ( "github.com/pkg/errors" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" ) type CurrentOp struct{} func (c CurrentOp) Run(stdout, _ io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubens/fzf.go b/cmd/kubens/fzf.go index 8446c627..89b91e0a 100644 --- a/cmd/kubens/fzf.go +++ b/cmd/kubens/fzf.go @@ -23,7 +23,7 @@ type InteractiveSwitchOp struct { // TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go. func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { // parse kubeconfig just to see if it can be loaded - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) if err := kc.Parse(); err != nil { if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") diff --git a/cmd/kubens/list.go b/cmd/kubens/list.go index c4681993..e858c4df 100644 --- a/cmd/kubens/list.go +++ b/cmd/kubens/list.go @@ -11,7 +11,6 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -19,7 +18,7 @@ import ( type ListOp struct{} func (op ListOp) Run(stdout, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubens/switch.go b/cmd/kubens/switch.go index 216f6c27..8f752413 100644 --- a/cmd/kubens/switch.go +++ b/cmd/kubens/switch.go @@ -8,7 +8,6 @@ import ( errors2 "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -18,7 +17,7 @@ type SwitchOp struct { } func (s SwitchOp) Run(_, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/internal/cmdutil/util.go b/internal/cmdutil/util.go new file mode 100644 index 00000000..3b2d449f --- /dev/null +++ b/internal/cmdutil/util.go @@ -0,0 +1,29 @@ +package cmdutil + +import ( + "os" + + "github.com/pkg/errors" +) + +func HomeDir() string { + if v := os.Getenv("XDG_CACHE_HOME"); v != "" { + return v + } + home := os.Getenv("HOME") + if home == "" { + home = os.Getenv("USERPROFILE") // windows + } + return home +} + +// IsNotFoundErr determines if the underlying error is os.IsNotExist. Right now +// errors from github.com/pkg/errors doesn't work with os.IsNotExist. +func IsNotFoundErr(err error) bool { + for e := err; e != nil; e = errors.Unwrap(e) { + if os.IsNotExist(e) { + return true + } + } + return false +} diff --git a/internal/cmdutil/util_test.go b/internal/cmdutil/util_test.go new file mode 100644 index 00000000..7630d497 --- /dev/null +++ b/internal/cmdutil/util_test.go @@ -0,0 +1,68 @@ +package cmdutil + +import ( + "testing" + + "github.com/ahmetb/kubectx/internal/testutil" +) + +func Test_homeDir(t *testing.T) { + type env struct{ k, v string } + cases := []struct { + name string + envs []env + want string + }{ + { + name: "XDG_CACHE_HOME precedence", + envs: []env{ + {"XDG_CACHE_HOME", "xdg"}, + {"HOME", "home"}, + }, + want: "xdg", + }, + { + name: "HOME over USERPROFILE", + envs: []env{ + {"HOME", "home"}, + {"USERPROFILE", "up"}, + }, + want: "home", + }, + { + name: "only USERPROFILE available", + envs: []env{ + {"XDG_CACHE_HOME", ""}, + {"HOME", ""}, + {"USERPROFILE", "up"}, + }, + want: "up", + }, + { + name: "none available", + envs: []env{ + {"XDG_CACHE_HOME", ""}, + {"HOME", ""}, + {"USERPROFILE", ""}, + }, + want: "", + }, + } + + for _, c := range cases { + t.Run(c.name, func(tt *testing.T) { + var unsets []func() + for _, e := range c.envs { + unsets = append(unsets, testutil.WithEnvVar(e.k, e.v)) + } + + got := HomeDir() + if got != c.want { + t.Errorf("expected:%q got:%q", c.want, got) + } + for _, u := range unsets { + u() + } + }) + } +} diff --git a/internal/kubeconfig/helper_test.go b/internal/kubeconfig/helper_test.go index 3732cc09..de3e9f8a 100644 --- a/internal/kubeconfig/helper_test.go +++ b/internal/kubeconfig/helper_test.go @@ -11,12 +11,14 @@ type MockKubeconfigLoader struct { out bytes.Buffer } -func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) } -func (t *MockKubeconfigLoader) Write(p []byte) (n int, err error) { return t.out.Write(p) } -func (t *MockKubeconfigLoader) Close() error { return nil } -func (t *MockKubeconfigLoader) Reset() error { return nil } -func (t *MockKubeconfigLoader) Load() (ReadWriteResetCloser, error) { return t, nil } -func (t *MockKubeconfigLoader) Output() string { return t.out.String() } +func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) } +func (t *MockKubeconfigLoader) Write(p []byte) (n int, err error) { return t.out.Write(p) } +func (t *MockKubeconfigLoader) Close() error { return nil } +func (t *MockKubeconfigLoader) Reset() error { return nil } +func (t *MockKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { + return []ReadWriteResetCloser{ReadWriteResetCloser(t)}, nil +} +func (t *MockKubeconfigLoader) Output() string { return t.out.String() } func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader { return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)} diff --git a/internal/kubeconfig/kubeconfig.go b/internal/kubeconfig/kubeconfig.go index 742a0934..4c1fc3c6 100644 --- a/internal/kubeconfig/kubeconfig.go +++ b/internal/kubeconfig/kubeconfig.go @@ -15,7 +15,7 @@ type ReadWriteResetCloser interface { } type Loader interface { - Load() (ReadWriteResetCloser, error) + Load() ([]ReadWriteResetCloser, error) } type Kubeconfig struct { @@ -38,11 +38,14 @@ func (k *Kubeconfig) Close() error { } func (k *Kubeconfig) Parse() error { - f, err := k.loader.Load() + files, err := k.loader.Load() if err != nil { return errors.Wrap(err, "failed to load") } + // TODO since we don't support multiple kubeconfig files at the moment, there's just 1 file + f := files[0] + k.f = f var v yaml.Node if err := yaml.NewDecoder(f).Decode(&v); err != nil { diff --git a/internal/cmdutil/kubeconfigloader.go b/internal/kubeconfig/kubeconfigloader.go similarity index 61% rename from internal/cmdutil/kubeconfigloader.go rename to internal/kubeconfig/kubeconfigloader.go index 5545f638..18d17cd4 100644 --- a/internal/cmdutil/kubeconfigloader.go +++ b/internal/kubeconfig/kubeconfigloader.go @@ -1,27 +1,27 @@ -package cmdutil +package kubeconfig import ( + "github.com/ahmetb/kubectx/internal/cmdutil" "os" "path/filepath" "github.com/pkg/errors" - - "github.com/ahmetb/kubectx/internal/kubeconfig" ) var ( - DefaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader) + DefaultLoader Loader = new(StandardKubeconfigLoader) ) type StandardKubeconfigLoader struct{} type kubeconfigFile struct{ *os.File } -func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) { +func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) { cfgPath, err := kubeconfigPath() if err != nil { return nil, errors.Wrap(err, "cannot determine kubeconfig path") } + f, err := os.OpenFile(cfgPath, os.O_RDWR, 0) if err != nil { if os.IsNotExist(err) { @@ -29,7 +29,9 @@ func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) } return nil, errors.Wrap(err, "failed to open file") } - return &kubeconfigFile{f}, nil + + // TODO we'll return all kubeconfig files when we start implementing multiple kubeconfig support + return []ReadWriteResetCloser{ReadWriteResetCloser(&kubeconfigFile{f})}, nil } func (kf *kubeconfigFile) Reset() error { @@ -52,31 +54,9 @@ func kubeconfigPath() (string, error) { } // default path - home := HomeDir() + home := cmdutil.HomeDir() if home == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } return filepath.Join(home, ".kube", "config"), nil } - -func HomeDir() string { - if v := os.Getenv("XDG_CACHE_HOME"); v != "" { - return v - } - home := os.Getenv("HOME") - if home == "" { - home = os.Getenv("USERPROFILE") // windows - } - return home -} - -// IsNotFoundErr determines if the underlying error is os.IsNotExist. Right now -// errors from github.com/pkg/errors doesn't work with os.IsNotExist. -func IsNotFoundErr(err error) bool { - for e := err; e != nil; e = errors.Unwrap(e) { - if os.IsNotExist(e) { - return true - } - } - return false -} diff --git a/internal/cmdutil/kubeconfigloader_test.go b/internal/kubeconfig/kubeconfigloader_test.go similarity index 54% rename from internal/cmdutil/kubeconfigloader_test.go rename to internal/kubeconfig/kubeconfigloader_test.go index 64dbc67b..ce08a42a 100644 --- a/internal/cmdutil/kubeconfigloader_test.go +++ b/internal/kubeconfig/kubeconfigloader_test.go @@ -1,76 +1,15 @@ -package cmdutil +package kubeconfig import ( + "github.com/ahmetb/kubectx/internal/cmdutil" "os" "path/filepath" "strings" "testing" - "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/testutil" ) -func Test_homeDir(t *testing.T) { - type env struct{ k, v string } - cases := []struct { - name string - envs []env - want string - }{ - { - name: "XDG_CACHE_HOME precedence", - envs: []env{ - {"XDG_CACHE_HOME", "xdg"}, - {"HOME", "home"}, - }, - want: "xdg", - }, - { - name: "HOME over USERPROFILE", - envs: []env{ - {"HOME", "home"}, - {"USERPROFILE", "up"}, - }, - want: "home", - }, - { - name: "only USERPROFILE available", - envs: []env{ - {"XDG_CACHE_HOME", ""}, - {"HOME", ""}, - {"USERPROFILE", "up"}, - }, - want: "up", - }, - { - name: "none available", - envs: []env{ - {"XDG_CACHE_HOME", ""}, - {"HOME", ""}, - {"USERPROFILE", ""}, - }, - want: "", - }, - } - - for _, c := range cases { - t.Run(c.name, func(tt *testing.T) { - var unsets []func() - for _, e := range c.envs { - unsets = append(unsets, testutil.WithEnvVar(e.k, e.v)) - } - - got := HomeDir() - if got != c.want { - t.Errorf("expected:%q got:%q", c.want, got) - } - for _, u := range unsets { - u() - } - }) - } -} - func Test_kubeconfigPath(t *testing.T) { defer testutil.WithEnvVar("HOME", "/x/y/z")() @@ -119,12 +58,12 @@ func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) { func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) { defer testutil.WithEnvVar("KUBECONFIG", "foo")() - kc := new(kubeconfig.Kubeconfig).WithLoader(DefaultLoader) + kc := new(Kubeconfig).WithLoader(DefaultLoader) err := kc.Parse() if err == nil { t.Fatal("expected err") } - if !IsNotFoundErr(err) { + if !cmdutil.IsNotFoundErr(err) { t.Fatalf("expected ENOENT error; got=%v", err) } }