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 68ea776826
commit 7c2cf62cf0
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"
)
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
}

View File

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

View File

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

View File

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

View File

@ -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 <NAME> : switch to context <NAME>
@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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