Structural refactoring for multiple kubeconfig support (#219)

This commit is contained in:
Sedat Gökcen 2020-06-02 22:04:13 +02:00 committed by GitHub
parent 170233bffd
commit 1db00a20d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 134 additions and 121 deletions

View File

@ -6,7 +6,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
) )
@ -14,7 +13,7 @@ import (
type CurrentOp struct{} type CurrentOp struct{}
func (_op CurrentOp) Run(stdout, _ io.Writer) error { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error") return errors.Wrap(err, "kubeconfig error")

View File

@ -5,7 +5,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer" "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 // deleteContext deletes a context entry by NAME or current-context
// indicated by ".". // indicated by ".".
func deleteContext(name string) (deleteName string, wasActiveContext bool, err error) { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return deleteName, false, errors.Wrap(err, "kubeconfig error") return deleteName, false, errors.Wrap(err, "kubeconfig error")

View File

@ -22,7 +22,7 @@ type InteractiveSwitchOp struct {
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded // 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 err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) { if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found") printer.Warning(stderr, "kubeconfig file not found")

View File

@ -16,7 +16,7 @@ import (
type ListOp struct{} type ListOp struct{}
func (_ ListOp) Run(stdout, stderr io.Writer) error { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) { if cmdutil.IsNotFoundErr(err) {

View File

@ -6,7 +6,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer" "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, // to the "new" value. If the old refers to the current-context,
// current-context preference is also updated. // current-context preference is also updated.
func (op RenameOp) Run(_, stderr io.Writer) error { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error") return errors.Wrap(err, "kubeconfig error")

View File

@ -5,7 +5,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer" "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") 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return "", errors.Wrap(err, "kubeconfig error") return "", errors.Wrap(err, "kubeconfig error")

View File

@ -5,7 +5,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer" "github.com/ahmetb/kubectx/internal/printer"
) )
@ -14,7 +13,7 @@ import (
type UnsetOp struct{} type UnsetOp struct{}
func (_ UnsetOp) Run(_, stderr io.Writer) error { func (_ UnsetOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error") return errors.Wrap(err, "kubeconfig error")

View File

@ -6,14 +6,13 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
) )
type CurrentOp struct{} type CurrentOp struct{}
func (c CurrentOp) Run(stdout, _ io.Writer) error { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error") return errors.Wrap(err, "kubeconfig error")

View File

@ -23,7 +23,7 @@ type InteractiveSwitchOp struct {
// TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go. // TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go.
func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error {
// parse kubeconfig just to see if it can be loaded // 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 err := kc.Parse(); err != nil {
if cmdutil.IsNotFoundErr(err) { if cmdutil.IsNotFoundErr(err) {
printer.Warning(stderr, "kubeconfig file not found") printer.Warning(stderr, "kubeconfig file not found")

View File

@ -11,7 +11,6 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth" _ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer" "github.com/ahmetb/kubectx/internal/printer"
) )
@ -19,7 +18,7 @@ import (
type ListOp struct{} type ListOp struct{}
func (op ListOp) Run(stdout, stderr io.Writer) error { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error") return errors.Wrap(err, "kubeconfig error")

View File

@ -8,7 +8,6 @@ import (
errors2 "k8s.io/apimachinery/pkg/api/errors" errors2 "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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/kubeconfig"
"github.com/ahmetb/kubectx/internal/printer" "github.com/ahmetb/kubectx/internal/printer"
) )
@ -18,7 +17,7 @@ type SwitchOp struct {
} }
func (s SwitchOp) Run(_, stderr io.Writer) error { 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() defer kc.Close()
if err := kc.Parse(); err != nil { if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error") return errors.Wrap(err, "kubeconfig error")

29
internal/cmdutil/util.go Normal file
View File

@ -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
}

View File

@ -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()
}
})
}
}

View File

@ -11,12 +11,14 @@ type MockKubeconfigLoader struct {
out bytes.Buffer out bytes.Buffer
} }
func (t *MockKubeconfigLoader) Read(p []byte) (n int, err error) { return t.in.Read(p) } 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) Write(p []byte) (n int, err error) { return t.out.Write(p) }
func (t *MockKubeconfigLoader) Close() error { return nil } func (t *MockKubeconfigLoader) Close() error { return nil }
func (t *MockKubeconfigLoader) Reset() error { return nil } func (t *MockKubeconfigLoader) Reset() error { return nil }
func (t *MockKubeconfigLoader) Load() (ReadWriteResetCloser, error) { return t, nil } func (t *MockKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
func (t *MockKubeconfigLoader) Output() string { return t.out.String() } return []ReadWriteResetCloser{ReadWriteResetCloser(t)}, nil
}
func (t *MockKubeconfigLoader) Output() string { return t.out.String() }
func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader { func WithMockKubeconfigLoader(kubecfg string) *MockKubeconfigLoader {
return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)} return &MockKubeconfigLoader{in: strings.NewReader(kubecfg)}

View File

@ -15,7 +15,7 @@ type ReadWriteResetCloser interface {
} }
type Loader interface { type Loader interface {
Load() (ReadWriteResetCloser, error) Load() ([]ReadWriteResetCloser, error)
} }
type Kubeconfig struct { type Kubeconfig struct {
@ -38,11 +38,14 @@ func (k *Kubeconfig) Close() error {
} }
func (k *Kubeconfig) Parse() error { func (k *Kubeconfig) Parse() error {
f, err := k.loader.Load() files, err := k.loader.Load()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to load") 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 k.f = f
var v yaml.Node var v yaml.Node
if err := yaml.NewDecoder(f).Decode(&v); err != nil { if err := yaml.NewDecoder(f).Decode(&v); err != nil {

View File

@ -1,27 +1,27 @@
package cmdutil package kubeconfig
import ( import (
"github.com/ahmetb/kubectx/internal/cmdutil"
"os" "os"
"path/filepath" "path/filepath"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/ahmetb/kubectx/internal/kubeconfig"
) )
var ( var (
DefaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader) DefaultLoader Loader = new(StandardKubeconfigLoader)
) )
type StandardKubeconfigLoader struct{} type StandardKubeconfigLoader struct{}
type kubeconfigFile struct{ *os.File } type kubeconfigFile struct{ *os.File }
func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) { func (*StandardKubeconfigLoader) Load() ([]ReadWriteResetCloser, error) {
cfgPath, err := kubeconfigPath() cfgPath, err := kubeconfigPath()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot determine kubeconfig path") return nil, errors.Wrap(err, "cannot determine kubeconfig path")
} }
f, err := os.OpenFile(cfgPath, os.O_RDWR, 0) f, err := os.OpenFile(cfgPath, os.O_RDWR, 0)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
@ -29,7 +29,9 @@ func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error)
} }
return nil, errors.Wrap(err, "failed to open file") 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 { func (kf *kubeconfigFile) Reset() error {
@ -52,31 +54,9 @@ func kubeconfigPath() (string, error) {
} }
// default path // default path
home := HomeDir() home := cmdutil.HomeDir()
if home == "" { if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set") return "", errors.New("HOME or USERPROFILE environment variable not set")
} }
return filepath.Join(home, ".kube", "config"), nil 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
}

View File

@ -1,76 +1,15 @@
package cmdutil package kubeconfig
import ( import (
"github.com/ahmetb/kubectx/internal/cmdutil"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/ahmetb/kubectx/internal/kubeconfig"
"github.com/ahmetb/kubectx/internal/testutil" "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) { func Test_kubeconfigPath(t *testing.T) {
defer testutil.WithEnvVar("HOME", "/x/y/z")() defer testutil.WithEnvVar("HOME", "/x/y/z")()
@ -119,12 +58,12 @@ func Test_kubeconfigPath_envOvverideDoesNotSupportPathSeparator(t *testing.T) {
func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) { func TestStandardKubeconfigLoader_returnsNotFoundErr(t *testing.T) {
defer testutil.WithEnvVar("KUBECONFIG", "foo")() defer testutil.WithEnvVar("KUBECONFIG", "foo")()
kc := new(kubeconfig.Kubeconfig).WithLoader(DefaultLoader) kc := new(Kubeconfig).WithLoader(DefaultLoader)
err := kc.Parse() err := kc.Parse()
if err == nil { if err == nil {
t.Fatal("expected err") t.Fatal("expected err")
} }
if !IsNotFoundErr(err) { if !cmdutil.IsNotFoundErr(err) {
t.Fatalf("expected ENOENT error; got=%v", err) t.Fatalf("expected ENOENT error; got=%v", err)
} }
} }