define Run(stdout,stderr) method on **Ops

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
Ahmet Alp Balkan 2020-04-12 12:29:08 -07:00
parent 5b3796ba1c
commit 28051b1fd7
No known key found for this signature in database
GPG Key ID: 441833503E604E2C
14 changed files with 140 additions and 173 deletions

View File

@ -7,7 +7,10 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func printCurrentContext(w io.Writer) error { // CurrentOp prints the current context
type CurrentOp struct{}
func (_op CurrentOp) Run(stdout, _ io.Writer) error {
cfgPath, err := kubeconfigPath() cfgPath, err := kubeconfigPath()
if err != nil { if err != nil {
return errors.Wrap(err, "failed to determine kubeconfig path") return errors.Wrap(err, "failed to determine kubeconfig path")
@ -22,6 +25,6 @@ func printCurrentContext(w io.Writer) error {
if v == "" { if v == "" {
return errors.New("current-context is not set") return errors.New("current-context is not set")
} }
_, err = fmt.Fprintln(w, v) _, err = fmt.Fprintln(stdout, v)
return err return err
} }

View File

@ -8,9 +8,14 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// DeleteOp indicates intention to delete contexts.
type DeleteOp struct {
Contexts []string // NAME or '.' to indicate current-context.
}
// deleteContexts deletes context entries one by one. // deleteContexts deletes context entries one by one.
func deleteContexts(w io.Writer, ctxs []string) error { func (op DeleteOp) Run(_, stderr io.Writer) error {
for _, ctx := range ctxs { for _, ctx := range op.Contexts {
// TODO inefficency here. we open/write/close the same file many times. // TODO inefficency here. we open/write/close the same file many times.
deletedName, wasActiveContext, err := deleteContext(ctx) deletedName, wasActiveContext, err := deleteContext(ctx)
if err != nil { if err != nil {
@ -20,7 +25,7 @@ func deleteContexts(w io.Writer, ctxs []string) error {
// TODO we don't always run as kubectx (sometimes "kubectl ctx") // TODO we don't always run as kubectx (sometimes "kubectl ctx")
printWarning("You deleted the current context. use \"kubectx\" to select a different one.") printWarning("You deleted the current context. use \"kubectx\" to select a different one.")
} }
fmt.Fprintf(w, "deleted context %q\n", deletedName) // TODO write with printSuccess (i.e. green) fmt.Fprintf(stderr, "deleted context %q\n", deletedName) // TODO write with printSuccess (i.e. green)
} }
return nil return nil
} }
@ -49,10 +54,6 @@ func deleteContext(name string) (deleteName string, wasActiveContext bool, err e
if err := modifyDocToDeleteContext(rootNode, name); err != nil { if err := modifyDocToDeleteContext(rootNode, name); err != nil {
return "", false, errors.Wrap(err, "failed to modify yaml doc") return "", false, errors.Wrap(err, "failed to modify yaml doc")
} }
if err := resetFile(f); err != nil {
return "", false, err
}
return name, wasActiveContext, errors.Wrap(saveKubeconfigRaw(f, rootNode), "failed to save kubeconfig file") return name, wasActiveContext, errors.Wrap(saveKubeconfigRaw(f, rootNode), "failed to save kubeconfig file")
} }

View File

@ -1,40 +1,23 @@
package main package main
import "strings" import (
"io"
"strings"
type Op interface{} "github.com/pkg/errors"
)
// HelpOp describes printing help. type Op interface {
type HelpOp struct{} Run(stdout, stderr io.Writer) error
// ListOp describes listing contexts.
type ListOp struct{}
// CurrentOp prints the current context
type CurrentOp struct{}
// SwitchOp indicates intention to switch contexts.
type SwitchOp struct {
Target string // '-' for back and forth, or NAME
} }
// UnsetOp indicates intention to remove current-context preference. // UnsupportedOp indicates an unsupported flag.
type UnsetOp struct{} type UnsupportedOp struct{ Err error }
// DeleteOp indicates intention to delete contexts. func (op UnsupportedOp) Run(_, _ io.Writer) error {
type DeleteOp struct { return op.Err
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 }
// parseArgs looks at flags (excl. executable name, i.e. argv[0]) // parseArgs looks at flags (excl. executable name, i.e. argv[0])
// and decides which operation should be taken. // and decides which operation should be taken.
func parseArgs(argv []string) Op { func parseArgs(argv []string) Op {
@ -43,8 +26,7 @@ func parseArgs(argv []string) Op {
} }
if argv[0] == "-d" { if argv[0] == "-d" {
ctxs := argv[1:] return DeleteOp{Contexts: argv[1:]}
return DeleteOp{ctxs}
} }
if len(argv) == 1 { if len(argv) == 1 {
@ -59,21 +41,16 @@ func parseArgs(argv []string) Op {
return UnsetOp{} return UnsetOp{}
} }
new, old, ok := parseRenameSyntax(v) // a=b a=. if new, old, ok := parseRenameSyntax(v); ok {
if ok { return RenameOp{New: new, Old: old}
return RenameOp{new, old}
} }
if strings.HasPrefix(v, "-") && v != "-" { if strings.HasPrefix(v, "-") && v != "-" {
return UnknownOp{argv} return UnsupportedOp{Err: errors.Errorf("unsupported option %s", v)}
} }
// TODO handle -d
// TODO handle -u/--unset
// TODO handle -c/--current
return SwitchOp{Target: argv[0]} return SwitchOp{Target: argv[0]}
} }
// TODO handle too many arguments e.g. "kubectx a b c" // TODO handle too many arguments e.g. "kubectx a b c"
return UnknownOp{} return UnsupportedOp{Err: errors.New("too many arguments")}
} }

View File

@ -59,8 +59,8 @@ func Test_parseArgs_new(t *testing.T) {
want: RenameOp{"a", "."}}, want: RenameOp{"a", "."}},
{name: "unrecognized flag", {name: "unrecognized flag",
args: []string{"-x"}, args: []string{"-x"},
want: UnknownOp{Args: []string{"-x"}}}, want: UnsupportedOp{Args: []string{"-x"}}},
// TODO add more UnknownOp cases // TODO add more UnsupportedOp cases
// TODO consider these cases // TODO consider these cases
// - kubectx foo --help // - kubectx foo --help

View File

@ -5,7 +5,14 @@ import (
"io" "io"
) )
func printHelp(out io.Writer) { // HelpOp describes printing help.
type HelpOp struct{}
func (_ HelpOp) Run(stdout, _ io.Writer) error {
return printUsage(stdout)
}
func printUsage(out io.Writer) error {
help := `USAGE: help := `USAGE:
kubectx : list the contexts kubectx : list the contexts
kubectx <NAME> : switch to context <NAME> kubectx <NAME> : switch to context <NAME>
@ -20,5 +27,6 @@ func printHelp(out io.Writer) {
kubectx -h,--help : show this message` kubectx -h,--help : show this message`
fmt.Fprintf(out, "%s\n", help) _, err := fmt.Fprintf(out, "%s\n", help)
return err
} }

View File

@ -29,7 +29,10 @@ func kubeconfigPath() (string, error) {
} }
func homeDir() string { func homeDir() string {
// TODO move tests out of kubeconfigPath to TestHomeDir() // TODO move tests for this out of kubeconfigPath to TestHomeDir()
if v := os.Getenv("XDG_CACHE_HOME"); v != "" {
return v
}
home := os.Getenv("HOME") home := os.Getenv("HOME")
if home == "" { if home == "" {
home = os.Getenv("USERPROFILE") // windows home = os.Getenv("USERPROFILE") // windows
@ -37,6 +40,8 @@ func homeDir() string {
return home return home
} }
// TODO parseKubeconfig doesn't seem necessary when there's a raw version that returns what's needed
func parseKubeconfig(path string) (kubeconfig, error) { func parseKubeconfig(path string) (kubeconfig, error) {
// TODO refactor to accept io.Reader instead of file // TODO refactor to accept io.Reader instead of file
var v kubeconfig var v kubeconfig

View File

@ -17,6 +17,14 @@ func parseKubeconfigRaw(r io.Reader) (*yaml.Node, error) {
} }
func saveKubeconfigRaw(w io.Writer, rootNode *yaml.Node) error { 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 := yaml.NewEncoder(w)
enc.SetIndent(2) enc.SetIndent(2)
return enc.Encode(rootNode) return enc.Encode(rootNode)
@ -39,14 +47,3 @@ func openKubeconfig() (f *os.File, rootNode *yaml.Node, err error) {
} }
return f, kc, nil return f, kc, nil
} }
// resetFile deletes contents of a file and sets the seek
// position to 0.
func resetFile(f *os.File) error {
if err := f.Truncate(0); err != nil {
return errors.Wrap(err, "failed to truncate")
}
_, err := f.Seek(0, 0)
return errors.Wrap(err, "failed to seek")
}

View File

@ -19,7 +19,10 @@ type kubeconfig struct {
Contexts []context `yaml:"contexts"` Contexts []context `yaml:"contexts"`
} }
func printListContexts(out io.Writer) error { // 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 // TODO extract printing and sorting into a function that's testable
cfgPath, err := kubeconfigPath() cfgPath, err := kubeconfigPath()
@ -41,12 +44,11 @@ func printListContexts(out io.Writer) error {
// TODO support KUBECTX_CURRENT_FGCOLOR // TODO support KUBECTX_CURRENT_FGCOLOR
// TODO support KUBECTX_CURRENT_BGCOLOR // TODO support KUBECTX_CURRENT_BGCOLOR
for _, c := range ctxs { for _, c := range ctxs {
out := c s := c
if c == cfg.CurrentContext { if c == cfg.CurrentContext {
out = color.New(color.FgYellow, color.Bold).Sprint(c) s = color.New(color.FgGreen, color.Bold).Sprint(c)
} }
fmt.Println(out) fmt.Fprintf(stdout, "%s\n", s)
} }
return nil return nil
} }

View File

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/fatih/color" "github.com/fatih/color"
) )
@ -13,56 +12,9 @@ func main() {
var op Op var op Op
op = parseArgs(os.Args[1:]) op = parseArgs(os.Args[1:])
// TODO consider addin Run() operation to each operation type if err := op.Run(os.Stdout, os.Stderr); err != nil {
switch v := op.(type) { printError(err.Error())
case HelpOp:
printHelp(os.Stdout)
case CurrentOp:
if err := printCurrentContext(os.Stdout); err != nil {
printError(err.Error())
os.Exit(1)
}
case UnsetOp:
if err := unsetContext(); err != nil {
printError(err.Error())
os.Exit(1)
}
case ListOp:
// TODO fzf installed show interactive selection
if err := printListContexts(os.Stdout); err != nil {
printError("%v", err)
os.Exit(1)
}
case DeleteOp:
if err := deleteContexts(os.Stderr, v.Contexts); err != nil {
printError("%v", err)
os.Exit(1)
}
case RenameOp:
if err := renameContexts(v.Old, v.New); err != nil {
printError("failed to rename: %v", err)
os.Exit(1)
}
case SwitchOp:
var newCtx string
var err error
if v.Target == "-" {
newCtx, err = swapContext()
} else {
newCtx, err = switchContext(v.Target)
}
if err != nil {
printError("failed to switch context: %v", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Switched to context %q.\n", newCtx)
case UnknownOp:
printError("unsupported operation: %s", strings.Join(v.Args, " "))
printHelp(os.Stdout)
os.Exit(1) os.Exit(1)
default:
fmt.Printf("internal error: operation type %T not handled", op)
} }
} }

View File

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"strings" "strings"
@ -9,6 +10,12 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// 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)
}
// parseRenameSyntax parses A=B form into [A,B] and returns // parseRenameSyntax parses A=B form into [A,B] and returns
// whether it is parsed correctly. // whether it is parsed correctly.
func parseRenameSyntax(v string) (string, string, bool) { func parseRenameSyntax(v string) (string, string, bool) {
@ -26,7 +33,7 @@ func parseRenameSyntax(v string) (string, string, bool) {
// rename changes the old (NAME or '.' for current-context) // rename changes the old (NAME or '.' for current-context)
// to the "new" value. If the old refers to the current-context, // to the "new" value. If the old refers to the current-context,
// current-context preference is also updated. // current-context preference is also updated.
func renameContexts(old, new string) error { func (op RenameOp) Run(_, _ io.Writer) error {
f, rootNode, err := openKubeconfig() f, rootNode, err := openKubeconfig()
if err != nil { if err != nil {
return nil return nil
@ -34,35 +41,29 @@ func renameContexts(old, new string) error {
defer f.Close() defer f.Close()
cur := getCurrentContext(rootNode) cur := getCurrentContext(rootNode)
if old == "." { if op.Old == "." {
old = cur op.Old = cur
} }
if !checkContextExists(rootNode, old) { if !checkContextExists(rootNode, op.Old) {
return errors.Errorf("context %q not found, can't rename it", old) return errors.Errorf("context %q not found, can't rename it", op.Old)
} }
if checkContextExists(rootNode, new) { if checkContextExists(rootNode, op.New) {
printWarning("context %q exists, overwriting it.", new) printWarning("context %q exists, overwriting it.", op.New)
if err := modifyDocToDeleteContext(rootNode, new); err != nil { if err := modifyDocToDeleteContext(rootNode, op.New); err != nil {
return errors.Wrap(err, "failed to delete new context to overwrite it") return errors.Wrap(err, "failed to delete new context to overwrite it")
} }
} }
if err := modifyContextName(rootNode, old, new); err != nil { if err := modifyContextName(rootNode, op.Old, op.New); err != nil {
return errors.Wrap(err, "failed to change context name") return errors.Wrap(err, "failed to change context name")
} }
if op.New == cur {
if old == cur { if err := modifyCurrentContext(rootNode, op.New); err != nil {
if err := modifyCurrentContext(rootNode, new); err != nil {
return errors.Wrap(err, "failed to set current-context to new name") return errors.Wrap(err, "failed to set current-context to new name")
} }
} }
// TODO the next two functions are always repeated.
if err := resetFile(f); err != nil {
return err
}
if err := saveKubeconfigRaw(f, rootNode); err != nil { if err := saveKubeconfigRaw(f, rootNode); err != nil {
return errors.Wrap(err, "failed to save modified kubeconfig") return errors.Wrap(err, "failed to save modified kubeconfig")
} }

View File

@ -8,7 +8,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func kubectxFilePath() (string, error) { func kubectxPrevCtxFile() (string, error) {
home := homeDir() home := homeDir()
if home == "" { if home == "" {
return "", errors.New("HOME or USERPROFILE environment variable not set") return "", errors.New("HOME or USERPROFILE environment variable not set")

View File

@ -64,7 +64,7 @@ func Test_kubectxFilePath(t *testing.T) {
defer os.Setenv("HOME", origHome) defer os.Setenv("HOME", origHome)
expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx") expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx")
v, err := kubectxFilePath() v, err := kubectxPrevCtxFile()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -81,7 +81,7 @@ func Test_kubectxFilePath_error(t *testing.T) {
defer os.Setenv("HOME", origHome) defer os.Setenv("HOME", origHome)
defer os.Setenv("USERPROFILE", origUserprofile) defer os.Setenv("USERPROFILE", origUserprofile)
_, err := kubectxFilePath() _, err := kubectxPrevCtxFile()
if err == nil { if err == nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,17 +1,40 @@
package main package main
import ( import (
"fmt"
"io"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// SwitchOp indicates intention to switch contexts.
type SwitchOp struct {
Target string // '-' for back and forth, or NAME
}
func (op SwitchOp) Run(stdout, stderr io.Writer) error {
var newCtx string
var err error
if op.Target == "-" {
newCtx, err = swapContext()
} else {
newCtx, err = switchContext(op.Target)
}
if err != nil {
return errors.Wrap(err, "failed to switch context")
}
// TODO use printSuccess when available.
fmt.Fprintf(stderr, "Switched to context %q.\n", newCtx)
return nil
}
// switchContext switches to specified context name. // switchContext switches to specified context name.
func switchContext(name string) (string, error) { func switchContext(name string) (string, error) {
stateFile, err := kubectxFilePath() prevCtxFile, err := kubectxPrevCtxFile()
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed to determine state file") return "", errors.Wrap(err, "failed to determine state file")
} }
f, kc, err := openKubeconfig() f, kc, err := openKubeconfig()
if err != nil { if err != nil {
return "", err return "", err
@ -19,33 +42,42 @@ func switchContext(name string) (string, error) {
defer f.Close() defer f.Close()
prev := getCurrentContext(kc) prev := getCurrentContext(kc)
if !checkContextExists(kc, name) {
// TODO: add a check to ensure user can't switch to non-existing context.
if !checkContextExists(kc, name) {
return "", errors.Errorf("no context exists with the name: %q", name) return "", errors.Errorf("no context exists with the name: %q", name)
} }
if err := modifyCurrentContext(kc, name); err != nil { if err := modifyCurrentContext(kc, name); err != nil {
return "", err return "", err
} }
if err := resetFile(f); err != nil {
return "", err
}
if err := saveKubeconfigRaw(f, kc); err != nil { if err := saveKubeconfigRaw(f, kc); err != nil {
return "", errors.Wrap(err, "failed to save kubeconfig") return "", errors.Wrap(err, "failed to save kubeconfig")
} }
if prev != name { if prev != name {
if err := writeLastContext(stateFile, prev); err != nil { if err := writeLastContext(prevCtxFile, prev); err != nil {
return "", errors.Wrap(err, "failed to save previous context name") return "", errors.Wrap(err, "failed to save previous context name")
} }
} }
return name, nil return name, nil
} }
// swapContext switches to previously switch context.
func swapContext() (string, error) {
prevCtxFile, err := kubectxPrevCtxFile()
if err != nil {
return "", errors.Wrap(err, "failed to determine state file")
}
prev, err := readLastContext(prevCtxFile)
if err != nil {
return "", errors.Wrap(err, "failed to read previous context file")
}
if prev == "" {
return "", errors.New("no previous context found")
}
return switchContext(prev)
}
func checkContextExists(rootNode *yaml.Node, name string) bool { func checkContextExists(rootNode *yaml.Node, name string) bool {
contexts := valueOf(rootNode, "contexts") contexts := valueOf(rootNode, "contexts")
if contexts == nil { if contexts == nil {
@ -83,22 +115,6 @@ func valueOf(mapNode *yaml.Node, key string) *yaml.Node {
return nil return nil
} }
// swapContext switches to previously switch context.
func swapContext() (string, error) {
stateFile, err := kubectxFilePath()
if err != nil {
return "", errors.Wrap(err, "failed to determine state file")
}
prev, err := readLastContext(stateFile)
if err != nil {
return "", errors.Wrap(err, "failed to read previous context file")
}
if prev == "" {
return "", errors.New("no previous context found")
}
return switchContext(prev)
}
// getCurrentContext returns "current-context" value in given // getCurrentContext returns "current-context" value in given
// kubeconfig object Node, or returns "" if not found. // kubeconfig object Node, or returns "" if not found.
func getCurrentContext(rootNode *yaml.Node) string { func getCurrentContext(rootNode *yaml.Node) string {

View File

@ -1,11 +1,17 @@
package main package main
import ( import (
"fmt"
"io"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func unsetContext() error { // UnsetOp indicates intention to remove current-context preference.
type UnsetOp struct{}
func (_ UnsetOp) Run(_, stderr io.Writer) error {
f, rootNode, err := openKubeconfig() f, rootNode, err := openKubeconfig()
if err != nil { if err != nil {
return err return err
@ -15,13 +21,12 @@ func unsetContext() error {
if err := modifyDocToUnsetContext(rootNode); err != nil { if err := modifyDocToUnsetContext(rootNode); err != nil {
return errors.Wrap(err, "error while modifying current-context") return errors.Wrap(err, "error while modifying current-context")
} }
if err := resetFile(f); err != nil {
return err
}
if err := saveKubeconfigRaw(f, rootNode); err != nil { if err := saveKubeconfigRaw(f, rootNode); err != nil {
return errors.Wrap(err, "failed to save kubeconfig file after modification") return errors.Wrap(err, "failed to save kubeconfig file after modification")
} }
return nil
_, err = fmt.Fprintln(stderr, "Successfully unset the current context")
return err
} }
func modifyDocToUnsetContext(rootNode *yaml.Node) error { func modifyDocToUnsetContext(rootNode *yaml.Node) error {