Add support for renaming contexts

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
Ahmet Alp Balkan 2020-04-10 16:11:38 -07:00
parent 5eabeab47e
commit cb103701ac
No known key found for this signature in database
GPG Key ID: 441833503E604E2C
7 changed files with 194 additions and 4 deletions

View File

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

View File

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

View File

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

View File

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

99
cmd/kubectx/rename.go Normal file
View File

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

View File

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

View File

@ -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",