mirror of
https://github.com/ahmetb/kubectx.git
synced 2025-07-31 23:00:38 +00:00
define Run(stdout,stderr) method on **Ops
Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
This commit is contained in:
parent
68ea776826
commit
7c2cf62cf0
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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")}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user