diff --git a/cmd/kubectx/main.go b/cmd/kubectx/main.go index c20551e..4c5585f 100644 --- a/cmd/kubectx/main.go +++ b/cmd/kubectx/main.go @@ -20,15 +20,25 @@ func main() { case ListOp: printListContexts(os.Stdout) case SwitchOp: - // TODO implement - panic("not implemented") + if v.Target == "-" { + // TODO implement swap + panic("not implemented") + } + newCtx, err := switchContext(v.Target) + if err != nil { + printError("faield to switch context: %v", err) + os.Exit(1) + } + fmt.Fprintf(os.Stderr, "Switched to context %q.\n", newCtx) case UnknownOp: - fmt.Printf("%s unsupported operation: %s\n", - color.RedString("error:"), - strings.Join(v.Args, " ")) + printError("unsupported operation: %s", strings.Join(v.Args, " ")) printHelp(os.Stdout) os.Exit(1) default: fmt.Printf("internal error: operation type %T not handled", op) } } + +func printError(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, color.RedString("error: "+format+"\n"), args...) +} diff --git a/cmd/kubectx/switch.go b/cmd/kubectx/switch.go new file mode 100644 index 0000000..4f8c020 --- /dev/null +++ b/cmd/kubectx/switch.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +// switchContext switches to specified context name. +func switchContext(name string) (string, error) { + cfgPath, err := kubeconfigPath() + if err != nil { + return "", errors.Wrap(err, "cannot determine kubeconfig path") + } + f, err := os.OpenFile(cfgPath, os.O_RDWR, 0) + if err != nil { + return "", errors.Wrap(err, "failed to open file") + } + defer f.Close() + + kc, err := parseKubeconfigRaw(f) + if err != nil { + return "", errors.Wrap(err, "yaml parse error") + } + + cur := getCurrentContext(kc) + fmt.Printf("current-context=%s\n", cur) + + if err := modifyCurrentContext(kc, name); err != nil { + return "", err + } + + 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") + } + + if err := saveKubeconfigRaw(f, kc); err != nil { + return "", errors.Wrap(err, "failed to save kubeconfig") + } + return name, 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 "" + } + for i, ch := range rootNode.Content { + if i%2 == 0 && ch.Value == "current-context" { + return rootNode.Content[i+1].Value + } + } + return "" +} + +func modifyCurrentContext(rootNode *yaml.Node, name string) error { + if rootNode.Kind != yaml.MappingNode { + return errors.New("document is not a map") + } + + // find current-context field => modify value (next children) + for i, ch := range rootNode.Content { + if i%2 == 0 && ch.Value == "current-context" { + rootNode.Content[i+1].Value = name + return nil + } + } + + // if current-context ==> create new field + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: "current-context", + Tag: "!!str"} + valueNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: name, + Tag: "!!str"} + rootNode.Content = append(rootNode.Content, keyNode, valueNode) + return nil +} + +func parseKubeconfigRaw(r io.Reader) (*yaml.Node, error) { + 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 { + enc := yaml.NewEncoder(w) + enc.SetIndent(2) + return enc.Encode(rootNode) +}