diff --git a/cmd/kubectx/current.go b/cmd/kubectx/current.go index 02dabcc..c426189 100644 --- a/cmd/kubectx/current.go +++ b/cmd/kubectx/current.go @@ -5,23 +5,22 @@ import ( "io" "github.com/pkg/errors" + + "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) // CurrentOp prints the current context type CurrentOp struct{} func (_op CurrentOp) Run(stdout, _ io.Writer) error { - cfgPath, err := kubeconfigPath() + kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + defer kc.Close() + rootNode, err := kc.ParseRaw() if err != nil { - return errors.Wrap(err, "failed to determine kubeconfig path") + return err } - cfg, err := parseKubeconfig(cfgPath) - if err != nil { - return errors.Wrap(err, "failed to read kubeconfig file") - } - - v := cfg.CurrentContext + v := kubeconfig.GetCurrentContext(rootNode) if v == "" { return errors.New("current-context is not set") } diff --git a/cmd/kubectx/delete.go b/cmd/kubectx/delete.go index 1c4164b..48c7aee 100644 --- a/cmd/kubectx/delete.go +++ b/cmd/kubectx/delete.go @@ -5,6 +5,8 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" + + "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) // DeleteOp indicates intention to delete contexts. @@ -33,13 +35,14 @@ 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) { - f, rootNode, err := openKubeconfig() + kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + defer kc.Close() + rootNode, err := kc.ParseRaw() if err != nil { return "", false, err } - defer f.Close() - cur := getCurrentContext(rootNode) + cur := kubeconfig.GetCurrentContext(rootNode) // resolve "." to a real name if name == "." { @@ -54,7 +57,7 @@ func deleteContext(name string) (deleteName string, wasActiveContext bool, err e if err := modifyDocToDeleteContext(rootNode, name); err != nil { return "", false, errors.Wrap(err, "failed to modify yaml doc") } - return name, wasActiveContext, errors.Wrap(saveKubeconfigRaw(f, rootNode), "failed to save kubeconfig file") + return name, wasActiveContext, errors.Wrap(kc.Save(), "failed to save kubeconfig file") } func modifyDocToDeleteContext(rootNode *yaml.Node, deleteName string) error { diff --git a/cmd/kubectx/kubeconfig.go b/cmd/kubectx/kubeconfig.go index 5df8aad..e78ad8f 100644 --- a/cmd/kubectx/kubeconfig.go +++ b/cmd/kubectx/kubeconfig.go @@ -10,7 +10,11 @@ import ( "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) -type defaultKubeconfigLoader struct{} +var ( + defaultLoader kubeconfig.Loader = new(StandardKubeconfigLoader) +) + +type StandardKubeconfigLoader struct{} type kubeconfigFile struct { *os.File } @@ -22,7 +26,7 @@ func (kf *kubeconfigFile) Reset() error { return errors.Wrap(err, "failed to seek in file") } -func (defaultKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) { +func (*StandardKubeconfigLoader) Load() (kubeconfig.ReadWriteResetCloser, error) { cfgPath, err := kubeconfigPath() if err != nil { return nil, errors.Wrap(err, "cannot determine kubeconfig path") diff --git a/cmd/kubectx/kubeconfig/helpers.go b/cmd/kubectx/kubeconfig/helpers.go new file mode 100644 index 0000000..a3b41b7 --- /dev/null +++ b/cmd/kubectx/kubeconfig/helpers.go @@ -0,0 +1,47 @@ +package kubeconfig + +import "gopkg.in/yaml.v3" + +func ContextNames(rootNode *yaml.Node) []string { + contexts := valueOf(rootNode, "contexts") + if contexts == nil { + return nil + } + if contexts.Kind != yaml.SequenceNode { + return nil + } + + var ctxNames []string + for _, ctx := range contexts.Content { + nameVal := valueOf(ctx, "name") + if nameVal != nil { + ctxNames = append(ctxNames, nameVal.Value) + } + } + return ctxNames +} + +// GetCurrentContext returns "current-context" value in given +// kubeconfig object Node, or returns "" if not found. +func GetCurrentContext(rootNode *yaml.Node) string { + if rootNode.Kind != yaml.MappingNode { + return "" + } + v := valueOf(rootNode, "current-context") + if v == nil { + return "" + } + return v.Value +} + +func valueOf(mapNode *yaml.Node, key string) *yaml.Node { + if mapNode.Kind != yaml.MappingNode { + return nil + } + for i, ch := range mapNode.Content { + if i%2 == 0 && ch.Kind == yaml.ScalarNode && ch.Value == key { + return mapNode.Content[i+1] + } + } + return nil +} diff --git a/cmd/kubectx/kubeconfig/kubeconfig.go b/cmd/kubectx/kubeconfig/kubeconfig.go index 89d4d79..c371609 100644 --- a/cmd/kubectx/kubeconfig/kubeconfig.go +++ b/cmd/kubectx/kubeconfig/kubeconfig.go @@ -25,8 +25,16 @@ type Kubeconfig struct { rootNode *yaml.Node } -func (k *Kubeconfig) WithLoader(l Loader) { +func (k *Kubeconfig) WithLoader(l Loader) *Kubeconfig { k.loader = l + return k +} + +func (k *Kubeconfig) Close() error { + if k.f == nil { + return nil + } + return k.f.Close() } func (k *Kubeconfig) ParseRaw() (*yaml.Node, error) { diff --git a/cmd/kubectx/kubeconfig_raw.go b/cmd/kubectx/kubeconfig_raw.go deleted file mode 100644 index d38a959..0000000 --- a/cmd/kubectx/kubeconfig_raw.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "io" - "os" - - "github.com/pkg/errors" - "gopkg.in/yaml.v3" -) - -func parseKubeconfigRaw(r io.Reader) (*yaml.Node, error) { - // TODO DELETE - var v yaml.Node - if err := yaml.NewDecoder(r).Decode(&v); err != nil { - return nil, err - } - return v.Content[0], nil -} - -func saveKubeconfigRaw(w io.Writer, rootNode *yaml.Node) error { - if f, ok := w.(*os.File); ok { - if err := f.Truncate(0); err != nil { - return errors.Wrap(err, "failed to truncate") - } - if _, err := f.Seek(0, 0); err != nil { - return errors.Wrap(err, "failed to seek") - } - } - enc := yaml.NewEncoder(w) - enc.SetIndent(2) - return enc.Encode(rootNode) -} - -func openKubeconfig() (f *os.File, rootNode *yaml.Node, err error) { - cfgPath, err := kubeconfigPath() - if err != nil { - return nil, nil, errors.Wrap(err, "cannot determine kubeconfig path") - } - f, err = os.OpenFile(cfgPath, os.O_RDWR, 0) - if err != nil { - return nil, nil, errors.Wrap(err, "failed to open file") - } - - kc, err := parseKubeconfigRaw(f) - if err != nil { - f.Close() - return nil, nil, errors.Wrap(err, "yaml parse error") - } - return f, kc, nil -} diff --git a/cmd/kubectx/list.go b/cmd/kubectx/list.go index 9be0425..ef41960 100644 --- a/cmd/kubectx/list.go +++ b/cmd/kubectx/list.go @@ -6,7 +6,8 @@ import ( "facette.io/natsort" "github.com/fatih/color" - "github.com/pkg/errors" + + "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) type context struct { @@ -22,30 +23,23 @@ type kubeconfigContents struct { // ListOp describes listing contexts. type ListOp struct{} -func (_ ListOp) Run(stdout, stderr io.Writer) error { - // TODO extract printing and sorting into a function that's testable - - cfgPath, err := kubeconfigPath() +func (_ ListOp) Run(stdout, _ io.Writer) error { + kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + defer kc.Close() + rootNode, err := kc.ParseRaw() if err != nil { - return errors.Wrap(err, "failed to determine kubeconfig path") + return err } - cfg, err := parseKubeconfig(cfgPath) - if err != nil { - return errors.Wrap(err, "failed to read kubeconfig file") - } - - ctxs := make([]string, 0, len(cfg.Contexts)) - for _, c := range cfg.Contexts { - ctxs = append(ctxs, c.Name) - } + ctxs := kubeconfig.ContextNames(rootNode) natsort.Sort(ctxs) // TODO support KUBECTX_CURRENT_FGCOLOR // TODO support KUBECTX_CURRENT_BGCOLOR + cur := kubeconfig.GetCurrentContext(rootNode) for _, c := range ctxs { s := c - if c == cfg.CurrentContext { + if c == cur { s = color.New(color.FgGreen, color.Bold).Sprint(c) } fmt.Fprintf(stdout, "%s\n", s) diff --git a/cmd/kubectx/rename.go b/cmd/kubectx/rename.go index e94909f..f2a38ad 100644 --- a/cmd/kubectx/rename.go +++ b/cmd/kubectx/rename.go @@ -6,6 +6,8 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" + + "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) // RenameOp indicates intention to rename contexts. @@ -32,13 +34,15 @@ 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 { - f, rootNode, err := openKubeconfig() - if err != nil { - return nil - } - defer f.Close() + kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + defer kc.Close() - cur := getCurrentContext(rootNode) + rootNode, err := kc.ParseRaw() + if err != nil { + return err + } + + cur := kubeconfig.GetCurrentContext(rootNode) if op.Old == "." { op.Old = cur } @@ -62,7 +66,7 @@ func (op RenameOp) Run(_, stderr io.Writer) error { return errors.Wrap(err, "failed to set current-context to new name") } } - if err := saveKubeconfigRaw(f, rootNode); err != nil { + if err := kc.Save(); err != nil { return errors.Wrap(err, "failed to save modified kubeconfig") } printSuccess(stderr, "Context %q renamed to %q.", op.Old, op.New) diff --git a/cmd/kubectx/switch.go b/cmd/kubectx/switch.go index c68cb8d..96117d7 100644 --- a/cmd/kubectx/switch.go +++ b/cmd/kubectx/switch.go @@ -5,6 +5,8 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" + + "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) // SwitchOp indicates intention to switch contexts. @@ -33,20 +35,23 @@ func switchContext(name string) (string, error) { if err != nil { return "", errors.Wrap(err, "failed to determine state file") } - f, kc, err := openKubeconfig() + + kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + defer kc.Close() + + rootNode, err := kc.ParseRaw() if err != nil { return "", err } - defer f.Close() - prev := getCurrentContext(kc) - if !checkContextExists(kc, name) { + prev := kubeconfig.GetCurrentContext(rootNode) + if !checkContextExists(rootNode, name) { return "", errors.Errorf("no context exists with the name: %q", name) } - if err := modifyCurrentContext(kc, name); err != nil { + if err := modifyCurrentContext(rootNode, name); err != nil { return "", err } - if err := saveKubeconfigRaw(f, kc); err != nil { + if err := kc.Save(); err != nil { return "", errors.Wrap(err, "failed to save kubeconfig") } @@ -77,22 +82,7 @@ func swapContext() (string, error) { func checkContextExists(rootNode *yaml.Node, name string) bool { - contexts := valueOf(rootNode, "contexts") - if contexts == nil { - return false - } - if contexts.Kind != yaml.SequenceNode { - return false - } - - var ctxNames []string - for _, ctx := range contexts.Content { - nameVal := valueOf(ctx, "name") - if nameVal != nil { - ctxNames = append(ctxNames, nameVal.Value) - } - } - + ctxNames := kubeconfig.ContextNames(rootNode) for _, v := range ctxNames { if v == name { return true @@ -101,6 +91,7 @@ func checkContextExists(rootNode *yaml.Node, name string) bool { return false } +// TODO delete func valueOf(mapNode *yaml.Node, key string) *yaml.Node { if mapNode.Kind != yaml.MappingNode { return nil @@ -113,18 +104,7 @@ func valueOf(mapNode *yaml.Node, key string) *yaml.Node { return nil } -// getCurrentContext returns "current-context" value in given -// kubeconfig object Node, or returns "" if not found. -func getCurrentContext(rootNode *yaml.Node) string { - if rootNode.Kind != yaml.MappingNode { - return "" - } - v := valueOf(rootNode, "current-context") - if v == nil { - return "" - } - return v.Value -} + func modifyCurrentContext(rootNode *yaml.Node, name string) error { if rootNode.Kind != yaml.MappingNode { diff --git a/cmd/kubectx/unset.go b/cmd/kubectx/unset.go index eae2399..cb12259 100644 --- a/cmd/kubectx/unset.go +++ b/cmd/kubectx/unset.go @@ -6,22 +6,26 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" + + "github.com/ahmetb/kubectx/cmd/kubectx/kubeconfig" ) // UnsetOp indicates intention to remove current-context preference. type UnsetOp struct{} func (_ UnsetOp) Run(_, stderr io.Writer) error { - f, rootNode, err := openKubeconfig() + kc := new(kubeconfig.Kubeconfig).WithLoader(defaultLoader) + defer kc.Close() + + rootNode, err := kc.ParseRaw() if err != nil { return err } - defer f.Close() if err := modifyDocToUnsetContext(rootNode); err != nil { return errors.Wrap(err, "error while modifying current-context") } - if err := saveKubeconfigRaw(f, rootNode); err != nil { + if err := kc.Save(); err != nil { return errors.Wrap(err, "failed to save kubeconfig file after modification") }