mirror of
https://github.com/ahmetb/kubectx.git
synced 2025-06-23 05:59:00 +00:00
Add support for renaming contexts
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
parent
5eabeab47e
commit
cb103701ac
@ -26,6 +26,12 @@ type DeleteOp struct {
|
|||||||
Contexts []string // NAME or '.' to indicate current-context.
|
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.
|
// UnknownOp indicates an unsupported flag.
|
||||||
type UnknownOp struct{ Args []string }
|
type UnknownOp struct{ Args []string }
|
||||||
|
|
||||||
@ -53,6 +59,11 @@ func parseArgs(argv []string) Op {
|
|||||||
return UnsetOp{}
|
return UnsetOp{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new, old, ok := parseRenameSyntax(v) // a=b a=.
|
||||||
|
if ok {
|
||||||
|
return RenameOp{new, old}
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(v, "-") && v != "-" {
|
if strings.HasPrefix(v, "-") && v != "-" {
|
||||||
return UnknownOp{argv}
|
return UnknownOp{argv}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,12 @@ func Test_parseArgs_new(t *testing.T) {
|
|||||||
{name: "delete - multiple contexts",
|
{name: "delete - multiple contexts",
|
||||||
args: []string{"-d", ".", "a", "b"},
|
args: []string{"-d", ".", "a", "b"},
|
||||||
want: DeleteOp{[]string{".", "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",
|
{name: "unrecognized flag",
|
||||||
args: []string{"-x"},
|
args: []string{"-x"},
|
||||||
want: UnknownOp{Args: []string{"-x"}}},
|
want: UnknownOp{Args: []string{"-x"}}},
|
||||||
|
@ -16,6 +16,6 @@ func TestPrintHelp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasSuffix(out, "\n") {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
case ListOp:
|
case ListOp:
|
||||||
if err := printListContexts(os.Stdout); err != nil {
|
if err := printListContexts(os.Stdout); err != nil {
|
||||||
printError(err.Error())
|
printError("%v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
case DeleteOp:
|
case DeleteOp:
|
||||||
if err := deleteContexts(os.Stderr, v.Contexts); err != nil {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
case SwitchOp:
|
case SwitchOp:
|
||||||
|
99
cmd/kubectx/rename.go
Normal file
99
cmd/kubectx/rename.go
Normal 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
|
||||||
|
}
|
69
cmd/kubectx/rename_test.go
Normal file
69
cmd/kubectx/rename_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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{
|
keyNode := &yaml.Node{
|
||||||
Kind: yaml.ScalarNode,
|
Kind: yaml.ScalarNode,
|
||||||
Value: "current-context",
|
Value: "current-context",
|
||||||
|
Loading…
Reference in New Issue
Block a user