From cb103701acff07e6d53f128edcf48b4ebfef7ca8 Mon Sep 17 00:00:00 2001 From: Ahmet Alp Balkan Date: Fri, 10 Apr 2020 16:11:38 -0700 Subject: [PATCH] Add support for renaming contexts Signed-off-by: Ahmet Alp Balkan --- cmd/kubectx/flags.go | 11 +++++ cmd/kubectx/flags_test.go | 6 +++ cmd/kubectx/help_test.go | 2 +- cmd/kubectx/main.go | 9 +++- cmd/kubectx/rename.go | 99 ++++++++++++++++++++++++++++++++++++++ cmd/kubectx/rename_test.go | 69 ++++++++++++++++++++++++++ cmd/kubectx/switch.go | 2 +- 7 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 cmd/kubectx/rename.go create mode 100644 cmd/kubectx/rename_test.go diff --git a/cmd/kubectx/flags.go b/cmd/kubectx/flags.go index 644e708..308faff 100644 --- a/cmd/kubectx/flags.go +++ b/cmd/kubectx/flags.go @@ -26,6 +26,12 @@ type DeleteOp struct { Contexts []string // NAME or '.' to indicate current-context. } +// RenameOp indicates intention to rename contexts. +type RenameOp struct { + New string // NAME of New context + Old string // NAME of Old context (or '.' for current-context) +} + // UnknownOp indicates an unsupported flag. type UnknownOp struct{ Args []string } @@ -53,6 +59,11 @@ func parseArgs(argv []string) Op { return UnsetOp{} } + new, old, ok := parseRenameSyntax(v) // a=b a=. + if ok { + return RenameOp{new, old} + } + if strings.HasPrefix(v, "-") && v != "-" { return UnknownOp{argv} } diff --git a/cmd/kubectx/flags_test.go b/cmd/kubectx/flags_test.go index f2b94ac..5ddf718 100644 --- a/cmd/kubectx/flags_test.go +++ b/cmd/kubectx/flags_test.go @@ -51,6 +51,12 @@ func Test_parseArgs_new(t *testing.T) { {name: "delete - multiple contexts", args: []string{"-d", ".", "a", "b"}, want: DeleteOp{[]string{".", "a", "b"}}}, + {name: "rename context", + args: []string{"a=b"}, + want: RenameOp{"a", "b"}}, + {name: "rename context with old=current", + args: []string{"a=."}, + want: RenameOp{"a", "."}}, {name: "unrecognized flag", args: []string{"-x"}, want: UnknownOp{Args: []string{"-x"}}}, diff --git a/cmd/kubectx/help_test.go b/cmd/kubectx/help_test.go index 22a4ebb..b8702e7 100644 --- a/cmd/kubectx/help_test.go +++ b/cmd/kubectx/help_test.go @@ -16,6 +16,6 @@ func TestPrintHelp(t *testing.T) { } if !strings.HasSuffix(out, "\n") { - t.Errorf("does not end with new line; output=%q", out) + t.Errorf("does not end with New line; output=%q", out) } } diff --git a/cmd/kubectx/main.go b/cmd/kubectx/main.go index 9693d40..8470cad 100644 --- a/cmd/kubectx/main.go +++ b/cmd/kubectx/main.go @@ -29,12 +29,17 @@ func main() { } case ListOp: if err := printListContexts(os.Stdout); err != nil { - printError(err.Error()) + printError("%v", err) os.Exit(1) } case DeleteOp: if err := deleteContexts(os.Stderr, v.Contexts); err != nil { - printError(err.Error()) + printError("%v", err) + os.Exit(1) + } + case RenameOp: + if err := rename(v.Old, v.New); err != nil { + printError("failed to rename: %v", err) os.Exit(1) } case SwitchOp: diff --git a/cmd/kubectx/rename.go b/cmd/kubectx/rename.go new file mode 100644 index 0000000..14fd3a6 --- /dev/null +++ b/cmd/kubectx/rename.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +// parseRenameSyntax parses A=B form into [A,B] and returns +// whether it is parsed correctly. +func parseRenameSyntax(v string) (string, string, bool) { + s := strings.Split(v, "=") + if len(s) != 2 { + return "", "", false + } + new, old := s[0], s[1] + if new == "" || old == "" { + return "", "", false + } + return new, old, true +} + +// rename changes the old (NAME or '.' for current-context) +// to the "new" value. If the old refers to the current-context, +// current-context preference is also updated. +func rename(old, new string) error { + f, rootNode, err := openKubeconfig() + if err != nil { + return nil + } + defer f.Close() + + cur := getCurrentContext(rootNode) + if old == "." { + old = cur + } + + if !checkContextExists(rootNode, old) { + return errors.Errorf("context %q not found, can't rename it", old) + } + + if checkContextExists(rootNode, new) { + printWarning("context %q exists, overwriting it.", new) + if err := modifyDocToDeleteContext(rootNode, new); err != nil { + return errors.Wrap(err, "failed to delete new context to overwrite it") + } + } + + if err := modifyContextName(rootNode, old, new); err != nil { + return errors.Wrap(err, "failed to change context name") + } + + if old == cur { + if err := modifyCurrentContext(rootNode, new); err != nil { + return errors.Wrap(err, "failed to set current-context to new name") + } + } + + if err := resetFile(f); err != nil { + return err + } + if err := saveKubeconfigRaw(f, rootNode); err != nil { + return errors.Wrap(err, "failed to save modified kubeconfig") + } + + return nil +} + +func modifyContextName(rootNode *yaml.Node, old, new string) error { + if rootNode.Kind != yaml.MappingNode { + return errors.New("root doc is not a mapping node") + } + contexts := valueOf(rootNode, "contexts") + if contexts == nil { + return errors.New("\"contexts\" entry is nil") + } else if contexts.Kind != yaml.SequenceNode { + return errors.New("\"contexts\" is not a sequence node") + } + + var changed bool + for _, contextNode := range contexts.Content { + nameNode := valueOf(contextNode, "name") + if nameNode.Kind == yaml.ScalarNode && nameNode.Value == old { + nameNode.Value = new + changed = true + break + } + } + if !changed { + return errors.New("no changes were made") + } + // TODO use printSuccess + // TODO consider moving printing logic to main + fmt.Fprintf(os.Stderr, "Context %q renamed to %q.\n", old, new) + return nil +} diff --git a/cmd/kubectx/rename_test.go b/cmd/kubectx/rename_test.go new file mode 100644 index 0000000..93362a0 --- /dev/null +++ b/cmd/kubectx/rename_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_parseRenameSyntax(t *testing.T) { + + type out struct { + New string + Old string + OK bool + } + tests := []struct { + name string + in string + want out + }{ + { + name: "no equals sign", + in: "foo", + want: out{OK: false}, + }, + { + name: "no left side", + in: "=a", + want: out{OK: false}, + }, + { + name: "no right side", + in: "a=", + want: out{OK: false}, + }, + { + name: "correct format", + in: "a=b", + want: out{ + New: "a", + Old: "b", + OK: true, + }, + }, + { + name: "correct format with current context", + in: "NEW_NAME=.", + want: out{ + New: "NEW_NAME", + Old: ".", + OK: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + new, old, ok := parseRenameSyntax(tt.in) + got := out{ + New: new, + Old: old, + OK: ok, + } + diff := cmp.Diff(tt.want, got) + if diff != "" { + t.Errorf("parseRenameSyntax() diff=%s", diff) + } + }) + } +} diff --git a/cmd/kubectx/switch.go b/cmd/kubectx/switch.go index 5b6f982..a3014f8 100644 --- a/cmd/kubectx/switch.go +++ b/cmd/kubectx/switch.go @@ -125,7 +125,7 @@ func modifyCurrentContext(rootNode *yaml.Node, name string) error { } } - // if current-context ==> create new field + // if current-context ==> create New field keyNode := &yaml.Node{ Kind: yaml.ScalarNode, Value: "current-context",