From c4252b579517fc484dcf37db36b910b4ca122da1 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Sat, 18 Apr 2020 13:32:53 -0700 Subject: [PATCH] Move kubeconfig loader utils to cmdutil pkg Signed-off-by: Ahmet Alp Balkan --- cmd/kubectx/current.go | 3 +- cmd/kubectx/delete.go | 3 +- cmd/kubectx/flags.go | 2 +- cmd/kubectx/fzf.go | 5 +- cmd/kubectx/list.go | 17 ++----- cmd/kubectx/rename.go | 3 +- cmd/kubectx/state.go | 4 +- cmd/kubectx/state_test.go | 5 +- cmd/kubectx/switch.go | 3 +- cmd/kubectx/unset.go | 3 +- cmd/kubens/current.go | 29 ++++++++++-- internal/cmdutil/interactive.go | 2 +- .../cmdutil/kubeconfigloader.go | 35 +++++++++----- .../cmdutil/kubeconfigloader_test.go | 27 ++--------- internal/kubeconfig/helpers_test.go | 1 - internal/kubeconfig/namespace.go | 39 ++++++++++++++++ internal/kubeconfig/namespace_test.go | 46 +++++++++++++++++++ internal/testutil/kubeconfigbuilder.go | 3 +- internal/testutil/tempfile.go | 26 +++++++++++ 19 files changed, 191 insertions(+), 65 deletions(-) rename cmd/kubectx/kubeconfig.go => internal/cmdutil/kubeconfigloader.go (78%) rename cmd/kubectx/kubeconfig_test.go => internal/cmdutil/kubeconfigloader_test.go (82%) delete mode 100644 internal/kubeconfig/helpers_test.go create mode 100644 internal/kubeconfig/namespace.go create mode 100644 internal/kubeconfig/namespace_test.go create mode 100644 internal/testutil/tempfile.go diff --git a/cmd/kubectx/current.go b/cmd/kubectx/current.go index 6155136..c8f0715 100644 --- a/cmd/kubectx/current.go +++ b/cmd/kubectx/current.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" + "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" ) @@ -13,7 +14,7 @@ import ( type CurrentOp struct{} func (_op CurrentOp) Run(stdout, _ io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.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 b330a4e..6ed8893 100644 --- a/cmd/kubectx/delete.go +++ b/cmd/kubectx/delete.go @@ -5,6 +5,7 @@ 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 +36,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(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return deleteName, false, errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubectx/flags.go b/cmd/kubectx/flags.go index 0b6982b..d7fc172 100644 --- a/cmd/kubectx/flags.go +++ b/cmd/kubectx/flags.go @@ -20,7 +20,7 @@ func (op UnsupportedOp) Run(_, _ io.Writer) error { // and decides which operation should be taken. func parseArgs(argv []string) Op { if len(argv) == 0 { - if env.IsInteractiveMode(os.Stdout) { + if cmdutil.IsInteractiveMode(os.Stdout) { return InteractiveSwitchOp{SelfCmd: os.Args[0]} } return ListOp{} diff --git a/cmd/kubectx/fzf.go b/cmd/kubectx/fzf.go index 851cf42..eefb398 100644 --- a/cmd/kubectx/fzf.go +++ b/cmd/kubectx/fzf.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" + "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" @@ -21,9 +22,9 @@ 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(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) if err := kc.Parse(); err != nil { - if isENOENT(err) { + if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } diff --git a/cmd/kubectx/list.go b/cmd/kubectx/list.go index e39d063..9df0534 100644 --- a/cmd/kubectx/list.go +++ b/cmd/kubectx/list.go @@ -3,12 +3,12 @@ package main import ( "fmt" "io" - "os" "facette.io/natsort" "github.com/fatih/color" "github.com/pkg/errors" + "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -17,10 +17,10 @@ import ( type ListOp struct{} func (_ ListOp) Run(stdout, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { - if isENOENT(err) { + if cmdutil.IsNotFoundErr(err) { printer.Warning(stderr, "kubeconfig file not found") return nil } @@ -50,14 +50,3 @@ func (_ ListOp) Run(stdout, stderr io.Writer) error { } return nil } - -// isENOENT determines if the underlying error is os.IsNotExist. Right now -// errors from github.com/pkg/errors doesn't work with os.IsNotExist. -func isENOENT(err error) bool { - for e := err; e != nil; e = errors.Unwrap(e) { - if os.IsNotExist(e) { - return true - } - } - return false -} diff --git a/cmd/kubectx/rename.go b/cmd/kubectx/rename.go index e57a305..4ed99b4 100644 --- a/cmd/kubectx/rename.go +++ b/cmd/kubectx/rename.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" + "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -34,7 +35,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(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) defer kc.Close() if err := kc.Parse(); err != nil { return errors.Wrap(err, "kubeconfig error") diff --git a/cmd/kubectx/state.go b/cmd/kubectx/state.go index 75a8863..a5963dc 100644 --- a/cmd/kubectx/state.go +++ b/cmd/kubectx/state.go @@ -6,10 +6,12 @@ import ( "path/filepath" "github.com/pkg/errors" + + "github.com/ahmetb/kubectx/internal/cmdutil" ) func kubectxPrevCtxFile() (string, error) { - home := homeDir() + home := cmdutil.HomeDir() if home == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } diff --git a/cmd/kubectx/state_test.go b/cmd/kubectx/state_test.go index 258bdfc..1616214 100644 --- a/cmd/kubectx/state_test.go +++ b/cmd/kubectx/state_test.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "testing" + + "github.com/ahmetb/kubectx/internal/testutil" ) func Test_readLastContext_nonExistingFile(t *testing.T) { @@ -18,7 +20,7 @@ func Test_readLastContext_nonExistingFile(t *testing.T) { } func Test_readLastContext(t *testing.T) { - path, cleanup := testfile(t, "foo") + path, cleanup := testutil.TempFile(t, "foo") defer cleanup() s, err := readLastContext(path) @@ -86,3 +88,4 @@ func Test_kubectxFilePath_error(t *testing.T) { t.Fatal(err) } } + diff --git a/cmd/kubectx/switch.go b/cmd/kubectx/switch.go index fa744eb..af40d6e 100644 --- a/cmd/kubectx/switch.go +++ b/cmd/kubectx/switch.go @@ -5,6 +5,7 @@ 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 +37,7 @@ func switchContext(name string) (string, error) { return "", errors.Wrap(err, "failed to determine state file") } - kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.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 4d94f7c..df433bf 100644 --- a/cmd/kubectx/unset.go +++ b/cmd/kubectx/unset.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" + "github.com/ahmetb/kubectx/internal/cmdutil" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) @@ -13,7 +14,7 @@ import ( type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { - kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.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 5139e97..9f05131 100644 --- a/cmd/kubens/current.go +++ b/cmd/kubens/current.go @@ -1,9 +1,32 @@ package main -import "io" +import ( + "fmt" + "io" + + "github.com/pkg/errors" + + "github.com/ahmetb/kubectx/internal/cmdutil" + "github.com/ahmetb/kubectx/internal/kubeconfig" +) type CurrentOp struct{} -func (c CurrentOp) Run(stdout, stderr io.Writer) error { - panic("implement me") +func (c CurrentOp) Run(stdout, _ io.Writer) error { + kc := new(kubeconfig.Kubeconfig).WithLoader(cmdutil.DefaultLoader) + defer kc.Close() + if err := kc.Parse(); err != nil { + return errors.Wrap(err, "kubeconfig error") + } + + ctx := kc.GetCurrentContext() + if ctx == "" { + return errors.New("current-context is not set") + } + ns, err := kc.NamespaceOfContext(ctx) + if err != nil { + return errors.Wrapf(err, "failed to read namespace of %q", ctx) + } + _, err = fmt.Fprintln(stdout, ns) + return errors.Wrap(err, "write error") } diff --git a/internal/cmdutil/interactive.go b/internal/cmdutil/interactive.go index 5a081e9..7a2d8fd 100644 --- a/internal/cmdutil/interactive.go +++ b/internal/cmdutil/interactive.go @@ -1,4 +1,4 @@ -package env +package cmdutil import ( "os" diff --git a/cmd/kubectx/kubeconfig.go b/internal/cmdutil/kubeconfigloader.go similarity index 78% rename from cmd/kubectx/kubeconfig.go rename to internal/cmdutil/kubeconfigloader.go index d3d5e05..5545f63 100644 --- a/cmd/kubectx/kubeconfig.go +++ b/internal/cmdutil/kubeconfigloader.go @@ -1,4 +1,4 @@ -package main +package cmdutil import ( "os" @@ -10,21 +10,13 @@ import ( ) var ( - defaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader) + DefaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader) ) type StandardKubeconfigLoader struct{} type kubeconfigFile struct{ *os.File } -func (kf *kubeconfigFile) Reset() error { - if err := kf.Truncate(0); err != nil { - return errors.Wrap(err, "failed to truncate file") - } - _, err := kf.Seek(0, 0) - return errors.Wrap(err, "failed to seek in file") -} - func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) { cfgPath, err := kubeconfigPath() if err != nil { @@ -40,6 +32,14 @@ func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) return &kubeconfigFile{f}, nil } +func (kf *kubeconfigFile) Reset() error { + if err := kf.Truncate(0); err != nil { + return errors.Wrap(err, "failed to truncate file") + } + _, err := kf.Seek(0, 0) + return errors.Wrap(err, "failed to seek in file") +} + func kubeconfigPath() (string, error) { // KUBECONFIG env var if v := os.Getenv("KUBECONFIG"); v != "" { @@ -52,14 +52,14 @@ func kubeconfigPath() (string, error) { } // default path - home := homeDir() + home := HomeDir() if home == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") } return filepath.Join(home, ".kube", "config"), nil } -func homeDir() string { +func HomeDir() string { if v := os.Getenv("XDG_CACHE_HOME"); v != "" { return v } @@ -69,3 +69,14 @@ func homeDir() string { } 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/cmd/kubectx/kubeconfig_test.go b/internal/cmdutil/kubeconfigloader_test.go similarity index 82% rename from cmd/kubectx/kubeconfig_test.go rename to internal/cmdutil/kubeconfigloader_test.go index f344104..64dbc67 100644 --- a/cmd/kubectx/kubeconfig_test.go +++ b/internal/cmdutil/kubeconfigloader_test.go @@ -1,7 +1,6 @@ -package main +package cmdutil import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -61,7 +60,7 @@ func Test_homeDir(t *testing.T) { unsets = append(unsets, testutil.WithEnvVar(e.k, e.v)) } - got := homeDir() + got := HomeDir() if got != c.want { t.Errorf("expected:%q got:%q", c.want, got) } @@ -120,30 +119,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.Kubeconfig).WithLoader(DefaultLoader) err := kc.Parse() if err == nil { t.Fatal("expected err") } - if !isENOENT(err) { + if !IsNotFoundErr(err) { t.Fatalf("expected ENOENT error; got=%v", err) } } - -func testfile(t *testing.T, contents string) (path string, cleanup func()) { - t.Helper() - - f, err := ioutil.TempFile(os.TempDir(), "test-file") - if err != nil { - t.Fatalf("failed to create test file: %v", err) - } - path = f.Name() - if _, err := f.Write([]byte(contents)); err != nil { - t.Fatalf("failed to write to test file: %v", err) - } - - return path, func() { - f.Close() - os.Remove(path) - } -} diff --git a/internal/kubeconfig/helpers_test.go b/internal/kubeconfig/helpers_test.go deleted file mode 100644 index f263a6d..0000000 --- a/internal/kubeconfig/helpers_test.go +++ /dev/null @@ -1 +0,0 @@ -package kubeconfig diff --git a/internal/kubeconfig/namespace.go b/internal/kubeconfig/namespace.go new file mode 100644 index 0000000..35852fa --- /dev/null +++ b/internal/kubeconfig/namespace.go @@ -0,0 +1,39 @@ +package kubeconfig + +import ( + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +const ( + defaultNamespace = "default" +) + +func (k *Kubeconfig) contextNode(name string) (*yaml.Node, error) { + contexts := valueOf(k.rootNode, "contexts") + if contexts == nil { + return nil, errors.New("\"contexts\" entry is nil") + } else if contexts.Kind != yaml.SequenceNode { + return nil, errors.New("\"contexts\" is not a sequence node") + } + + for _, contextNode := range contexts.Content { + nameNode := valueOf(contextNode, "name") + if nameNode.Kind == yaml.ScalarNode && nameNode.Value == name { + return contextNode, nil + } + } + return nil, errors.Errorf("context with name %q not found", name) +} + +func (k *Kubeconfig) NamespaceOfContext(contextName string) (string, error) { + ctx, err := k.contextNode(contextName) + if err != nil { + return "", err + } + ns := valueOf(ctx, "namespace") + if ns == nil || ns.Value == "" { + return defaultNamespace, nil + } + return ns.Value, nil +} diff --git a/internal/kubeconfig/namespace_test.go b/internal/kubeconfig/namespace_test.go new file mode 100644 index 0000000..ff663bb --- /dev/null +++ b/internal/kubeconfig/namespace_test.go @@ -0,0 +1,46 @@ +package kubeconfig + +import ( + "testing" + + "github.com/ahmetb/kubectx/internal/testutil" +) + +func TestKubeconfig_NamespaceOfContext_ctxNotFound(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs(testutil.Ctx("c1")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + _, err := kc.NamespaceOfContext("c2") + if err == nil { + t.Fatal("expected err") + } +} + +func TestKubeconfig_NamespaceOfContext(t *testing.T) { + kc := new(Kubeconfig).WithLoader(WithMockKubeconfigLoader(testutil.KC(). + WithCtxs( + testutil.Ctx("c1"), + testutil.Ctx("c2").Ns("c2n1")).ToYAML(t))) + if err := kc.Parse(); err != nil { + t.Fatal(err) + } + + v1, err := kc.NamespaceOfContext("c1") + if err != nil { + t.Fatal("expected err") + } + if expected := `default`; v1 != expected { + t.Fatalf("c1: expected=%q got=%q", expected, v1) + } + + v2, err := kc.NamespaceOfContext("c2") + if err != nil { + t.Fatal("expected err") + } + if expected := `c2n1`; v2 != expected { + t.Fatalf("c2: expected=%q got=%q", expected, v2) + } +} diff --git a/internal/testutil/kubeconfigbuilder.go b/internal/testutil/kubeconfigbuilder.go index 47cfe15..8fdb0d9 100644 --- a/internal/testutil/kubeconfigbuilder.go +++ b/internal/testutil/kubeconfigbuilder.go @@ -12,7 +12,8 @@ type Context struct { Namespace string `yaml:"namespace,omitempty"` } -func Ctx(name string) *Context { return &Context{Name: name} } +func Ctx(name string) *Context { return &Context{Name: name} } +func (c *Context) Ns(ns string) *Context { c.Namespace = ns; return c } type Kubeconfig map[string]interface{} diff --git a/internal/testutil/tempfile.go b/internal/testutil/tempfile.go new file mode 100644 index 0000000..e04bf70 --- /dev/null +++ b/internal/testutil/tempfile.go @@ -0,0 +1,26 @@ +package testutil + +import ( + "io/ioutil" + "os" + "testing" +) + +func TempFile(t *testing.T, contents string) (path string, cleanup func()) { + // TODO consider removing, used only in one place. + t.Helper() + + f, err := ioutil.TempFile(os.TempDir(), "test-file") + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + path = f.Name() + if _, err := f.Write([]byte(contents)); err != nil { + t.Fatalf("failed to write to test file: %v", err) + } + + return path, func() { + f.Close() + os.Remove(path) + } +}