diff --git a/cmd/kubectx/current.go b/cmd/kubectx/current.go index 6a9aa2d..02dabcc 100644 --- a/cmd/kubectx/current.go +++ b/cmd/kubectx/current.go @@ -7,7 +7,10 @@ import ( "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() if err != nil { return errors.Wrap(err, "failed to determine kubeconfig path") @@ -22,6 +25,6 @@ func printCurrentContext(w io.Writer) error { if v == "" { return errors.New("current-context is not set") } - _, err = fmt.Fprintln(w, v) + _, err = fmt.Fprintln(stdout, v) return err } diff --git a/cmd/kubectx/delete.go b/cmd/kubectx/delete.go index 47f47c7..13c875d 100644 --- a/cmd/kubectx/delete.go +++ b/cmd/kubectx/delete.go @@ -8,9 +8,14 @@ import ( "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. -func deleteContexts(w io.Writer, ctxs []string) error { - for _, ctx := range ctxs { +func (op DeleteOp) Run(_, stderr io.Writer) error { + for _, ctx := range op.Contexts { // TODO inefficency here. we open/write/close the same file many times. deletedName, wasActiveContext, err := deleteContext(ctx) 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") 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 } @@ -49,10 +54,6 @@ func deleteContext(name string) (deleteName string, wasActiveContext bool, err e if err := modifyDocToDeleteContext(rootNode, name); err != nil { 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") } diff --git a/cmd/kubectx/flags.go b/cmd/kubectx/flags.go index 308faff..2ce1bf0 100644 --- a/cmd/kubectx/flags.go +++ b/cmd/kubectx/flags.go @@ -1,40 +1,23 @@ package main -import "strings" +import ( + "io" + "strings" -type Op interface{} + "github.com/pkg/errors" +) -// HelpOp describes printing help. -type HelpOp struct{} - -// 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 +type Op interface { + Run(stdout, stderr io.Writer) error } -// UnsetOp indicates intention to remove current-context preference. -type UnsetOp struct{} +// UnsupportedOp indicates an unsupported flag. +type UnsupportedOp struct{ Err error } -// DeleteOp indicates intention to delete contexts. -type DeleteOp struct { - Contexts []string // NAME or '.' to indicate current-context. +func (op UnsupportedOp) Run(_, _ io.Writer) error { + return op.Err } -// 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]) // and decides which operation should be taken. func parseArgs(argv []string) Op { @@ -43,8 +26,7 @@ func parseArgs(argv []string) Op { } if argv[0] == "-d" { - ctxs := argv[1:] - return DeleteOp{ctxs} + return DeleteOp{Contexts: argv[1:]} } if len(argv) == 1 { @@ -59,21 +41,16 @@ func parseArgs(argv []string) Op { return UnsetOp{} } - new, old, ok := parseRenameSyntax(v) // a=b a=. - if ok { - return RenameOp{new, old} + if new, old, ok := parseRenameSyntax(v); ok { + return RenameOp{New: new, Old: old} } 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]} } // TODO handle too many arguments e.g. "kubectx a b c" - return UnknownOp{} + return UnsupportedOp{Err: errors.New("too many arguments")} } diff --git a/cmd/kubectx/flags_test.go b/cmd/kubectx/flags_test.go index 5ddf718..3ca5477 100644 --- a/cmd/kubectx/flags_test.go +++ b/cmd/kubectx/flags_test.go @@ -59,8 +59,8 @@ func Test_parseArgs_new(t *testing.T) { want: RenameOp{"a", "."}}, {name: "unrecognized flag", args: []string{"-x"}, - want: UnknownOp{Args: []string{"-x"}}}, - // TODO add more UnknownOp cases + want: UnsupportedOp{Args: []string{"-x"}}}, + // TODO add more UnsupportedOp cases // TODO consider these cases // - kubectx foo --help diff --git a/cmd/kubectx/help.go b/cmd/kubectx/help.go index 38a41cc..dc0e437 100644 --- a/cmd/kubectx/help.go +++ b/cmd/kubectx/help.go @@ -5,7 +5,14 @@ import ( "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: kubectx : list the contexts kubectx : switch to context @@ -20,5 +27,6 @@ func printHelp(out io.Writer) { kubectx -h,--help : show this message` - fmt.Fprintf(out, "%s\n", help) + _, err := fmt.Fprintf(out, "%s\n", help) + return err } diff --git a/cmd/kubectx/kubeconfig.go b/cmd/kubectx/kubeconfig.go index 88b9abf..ac58bd2 100644 --- a/cmd/kubectx/kubeconfig.go +++ b/cmd/kubectx/kubeconfig.go @@ -29,7 +29,10 @@ func kubeconfigPath() (string, error) { } 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") if home == "" { home = os.Getenv("USERPROFILE") // windows @@ -37,6 +40,8 @@ func homeDir() string { 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) { // TODO refactor to accept io.Reader instead of file var v kubeconfig diff --git a/cmd/kubectx/kubeconfig_raw.go b/cmd/kubectx/kubeconfig_raw.go index 53ed227..ffbc1fa 100644 --- a/cmd/kubectx/kubeconfig_raw.go +++ b/cmd/kubectx/kubeconfig_raw.go @@ -17,6 +17,14 @@ func parseKubeconfigRaw(r io.Reader) (*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.SetIndent(2) return enc.Encode(rootNode) @@ -39,14 +47,3 @@ func openKubeconfig() (f *os.File, rootNode *yaml.Node, err error) { } 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") -} diff --git a/cmd/kubectx/list.go b/cmd/kubectx/list.go index bdaf604..8777831 100644 --- a/cmd/kubectx/list.go +++ b/cmd/kubectx/list.go @@ -19,7 +19,10 @@ type kubeconfig struct { 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 cfgPath, err := kubeconfigPath() @@ -41,12 +44,11 @@ func printListContexts(out io.Writer) error { // TODO support KUBECTX_CURRENT_FGCOLOR // TODO support KUBECTX_CURRENT_BGCOLOR for _, c := range ctxs { - out := c + s := c 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 } - diff --git a/cmd/kubectx/main.go b/cmd/kubectx/main.go index 09d3ca8..5b1127f 100644 --- a/cmd/kubectx/main.go +++ b/cmd/kubectx/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "strings" "github.com/fatih/color" ) @@ -13,56 +12,9 @@ func main() { var op Op op = parseArgs(os.Args[1:]) - // TODO consider addin Run() operation to each operation type - switch v := op.(type) { - 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) + if err := op.Run(os.Stdout, os.Stderr); err != nil { + printError(err.Error()) os.Exit(1) - default: - fmt.Printf("internal error: operation type %T not handled", op) } } diff --git a/cmd/kubectx/rename.go b/cmd/kubectx/rename.go index aad6a91..c875bab 100644 --- a/cmd/kubectx/rename.go +++ b/cmd/kubectx/rename.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "io" "os" "strings" @@ -9,6 +10,12 @@ import ( "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 // whether it is parsed correctly. 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) // to the "new" value. If the old refers to the current-context, // current-context preference is also updated. -func renameContexts(old, new string) error { +func (op RenameOp) Run(_, _ io.Writer) error { f, rootNode, err := openKubeconfig() if err != nil { return nil @@ -34,35 +41,29 @@ func renameContexts(old, new string) error { defer f.Close() cur := getCurrentContext(rootNode) - if old == "." { - old = cur + if op.Old == "." { + op.Old = cur } - if !checkContextExists(rootNode, old) { - return errors.Errorf("context %q not found, can't rename it", old) + if !checkContextExists(rootNode, op.Old) { + return errors.Errorf("context %q not found, can't rename it", op.Old) } - if checkContextExists(rootNode, new) { - printWarning("context %q exists, overwriting it.", new) - if err := modifyDocToDeleteContext(rootNode, new); err != nil { + if checkContextExists(rootNode, op.New) { + printWarning("context %q exists, overwriting it.", op.New) + if err := modifyDocToDeleteContext(rootNode, op.New); err != nil { 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") } - - if old == cur { - if err := modifyCurrentContext(rootNode, new); err != nil { + if op.New == cur { + if err := modifyCurrentContext(rootNode, op.New); err != nil { 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 { return errors.Wrap(err, "failed to save modified kubeconfig") } diff --git a/cmd/kubectx/state.go b/cmd/kubectx/state.go index 1d0f334..75a8863 100644 --- a/cmd/kubectx/state.go +++ b/cmd/kubectx/state.go @@ -8,7 +8,7 @@ import ( "github.com/pkg/errors" ) -func kubectxFilePath() (string, error) { +func kubectxPrevCtxFile() (string, error) { home := homeDir() if home == "" { return "", errors.New("HOME or USERPROFILE environment variable not set") diff --git a/cmd/kubectx/state_test.go b/cmd/kubectx/state_test.go index b67fd3e..258bdfc 100644 --- a/cmd/kubectx/state_test.go +++ b/cmd/kubectx/state_test.go @@ -64,7 +64,7 @@ func Test_kubectxFilePath(t *testing.T) { defer os.Setenv("HOME", origHome) expected := filepath.Join(filepath.FromSlash("/foo/bar"), ".kube", "kubectx") - v, err := kubectxFilePath() + v, err := kubectxPrevCtxFile() if err != nil { t.Fatal(err) } @@ -81,7 +81,7 @@ func Test_kubectxFilePath_error(t *testing.T) { defer os.Setenv("HOME", origHome) defer os.Setenv("USERPROFILE", origUserprofile) - _, err := kubectxFilePath() + _, err := kubectxPrevCtxFile() if err == nil { t.Fatal(err) } diff --git a/cmd/kubectx/switch.go b/cmd/kubectx/switch.go index a3014f8..7cf5cc9 100644 --- a/cmd/kubectx/switch.go +++ b/cmd/kubectx/switch.go @@ -1,17 +1,40 @@ package main import ( + "fmt" + "io" + "github.com/pkg/errors" "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. func switchContext(name string) (string, error) { - stateFile, err := kubectxFilePath() + prevCtxFile, err := kubectxPrevCtxFile() if err != nil { return "", errors.Wrap(err, "failed to determine state file") } - f, kc, err := openKubeconfig() if err != nil { return "", err @@ -19,33 +42,42 @@ func switchContext(name string) (string, error) { defer f.Close() prev := getCurrentContext(kc) - - // TODO: add a check to ensure user can't switch to non-existing context. - if !checkContextExists(kc, name) { + if !checkContextExists(kc, name) { return "", errors.Errorf("no context exists with the name: %q", name) } - if err := modifyCurrentContext(kc, name); err != nil { return "", err } - - if err := resetFile(f); err != nil { - return "", err - } - if err := saveKubeconfigRaw(f, kc); err != nil { return "", errors.Wrap(err, "failed to save kubeconfig") } 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 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 { contexts := valueOf(rootNode, "contexts") if contexts == nil { @@ -83,22 +115,6 @@ func valueOf(mapNode *yaml.Node, key string) *yaml.Node { 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 // kubeconfig object Node, or returns "" if not found. func getCurrentContext(rootNode *yaml.Node) string { diff --git a/cmd/kubectx/unset.go b/cmd/kubectx/unset.go index c9002ee..eae2399 100644 --- a/cmd/kubectx/unset.go +++ b/cmd/kubectx/unset.go @@ -1,11 +1,17 @@ package main import ( + "fmt" + "io" + "github.com/pkg/errors" "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() if err != nil { return err @@ -15,13 +21,12 @@ func unsetContext() error { if err := modifyDocToUnsetContext(rootNode); err != nil { return errors.Wrap(err, "error while modifying current-context") } - if err := resetFile(f); err != nil { - return err - } if err := saveKubeconfigRaw(f, rootNode); err != nil { 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 {