diff --git a/cmd/main.go b/cmd/main.go index 86a56dc24..3d87c0029 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -24,27 +24,6 @@ import ( func main() { rootCommand := switcher.NewCommandStartSwitcher() - // if first argument is not found, assume it is a context name - // hence call default subcommand - if len(os.Args) > 1 { - cmd, _, err := rootCommand.Find(os.Args[1:]) - if err != nil || cmd == nil { - args := append([]string{"set-context"}, os.Args[1:]...) - rootCommand.SetArgs(args) - } - - // cobra somehow does not recognize - as a valid command - if os.Args[1] == "-" { - args := append([]string{"set-previous-context"}, os.Args[1:]...) - rootCommand.SetArgs(args) - } - - if os.Args[1] == "." { - args := append([]string{"set-last-context"}, os.Args[1:]...) - rootCommand.SetArgs(args) - } - } - if err := rootCommand.Execute(); err != nil { fmt.Print(err) os.Exit(1) diff --git a/cmd/switcher/alias.go b/cmd/switcher/alias.go new file mode 100644 index 000000000..eafd17f2b --- /dev/null +++ b/cmd/switcher/alias.go @@ -0,0 +1,72 @@ +package switcher + +import ( + "fmt" + "os" + "strings" + + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/alias" + "github.com/spf13/cobra" +) + +var ( + aliasContextCmd = &cobra.Command{ + Use: "alias", + Short: "Create an alias for a context. Use ALIAS=CONTEXT_NAME", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 || !strings.Contains(args[0], "=") || len(strings.Split(args[0], "=")) != 2 { + return fmt.Errorf("please provide the alias in the form ALIAS=CONTEXT_NAME") + } + + arguments := strings.Split(args[0], "=") + ctxName, err := resolveContextName(arguments[1]) + if err != nil { + return err + } + + stores, config, err := initialize() + if err != nil { + return err + } + + return alias.Alias(arguments[0], ctxName, stores, config, stateDirectory, noIndex) + }, + SilenceErrors: true, + } + + aliasLsCmd = &cobra.Command{ + Use: "ls", + Short: "List all existing aliases", + RunE: func(cmd *cobra.Command, args []string) error { + return alias.ListAliases(stateDirectory) + }, + } + + aliasRmCmd = &cobra.Command{ + Use: "rm", + Short: "Remove an existing alias", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return fmt.Errorf("please provide the alias to remove as the first argument") + } + + return alias.RemoveAlias(args[0], stateDirectory) + }, + SilenceErrors: true, + } +) + +func init() { + aliasRmCmd.Flags().StringVar( + &stateDirectory, + "state-directory", + os.ExpandEnv("$HOME/.kube/switch-state"), + "path to the state directory.") + + aliasContextCmd.AddCommand(aliasLsCmd) + aliasContextCmd.AddCommand(aliasRmCmd) + + setFlagsForContextCommands(aliasContextCmd) + + rootCommand.AddCommand(aliasContextCmd) +} diff --git a/cmd/switcher/clean.go b/cmd/switcher/clean.go new file mode 100644 index 000000000..6fa5f7875 --- /dev/null +++ b/cmd/switcher/clean.go @@ -0,0 +1,25 @@ +package switcher + +import ( + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/clean" + "github.com/spf13/cobra" +) + +var ( + cleanCmd = &cobra.Command{ + Use: "clean", + Short: "Cleans all temporary and cached kubeconfig files", + Long: `Cleans the temporary kubeconfig files created in the directory $HOME/.kube/switch_tmp and flushes every cache`, + RunE: func(cmd *cobra.Command, args []string) error { + stores, _, err := initialize() + if err != nil { + return err + } + return clean.Clean(stores) + }, + } +) + +func init() { + rootCommand.AddCommand(cleanCmd) +} diff --git a/cmd/switcher/completion.go b/cmd/switcher/completion.go new file mode 100644 index 000000000..187594734 --- /dev/null +++ b/cmd/switcher/completion.go @@ -0,0 +1,42 @@ +package switcher + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var ( + setName string + + completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish]", + Short: "generate completion script", + Long: "load the completion script for switch into the current shell", + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish"}, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + root := cmd.Root() + if setName != "" { + root.Use = setName + } + switch args[0] { + case "bash": + return root.GenBashCompletion(os.Stdout) + case "zsh": + return root.GenZshCompletion(os.Stdout) + case "fish": + return root.GenFishCompletion(os.Stdout, true) + } + return fmt.Errorf("unsupported shell type: %s", args[0]) + }, + } +) + +func init() { + completionCmd.Flags().StringVarP(&setName, "cmd", "c", "", "generate completion for the specified command") + + rootCommand.AddCommand(completionCmd) +} diff --git a/cmd/switcher/context.go b/cmd/switcher/context.go new file mode 100644 index 000000000..75b57bfd6 --- /dev/null +++ b/cmd/switcher/context.go @@ -0,0 +1,206 @@ +package switcher + +import ( + "fmt" + "os" + + delete_context "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/delete-context" + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/history" + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/hooks" + list_contexts "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/list-contexts" + set_context "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/set-context" + unset_context "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/unset-context" + "github.com/danielfoehrkn/kubeswitch/pkg/util" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + previousContextCmd = &cobra.Command{ + Use: "set-previous-context", + Short: "Switch to the previous context from the history", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + stores, config, err := initialize() + if err != nil { + return err + } + + kc, err := history.SetPreviousContext(stores, config, stateDirectory, noIndex) + reportNewContext(kc) + return err + }, + } + + lastContextCmd = &cobra.Command{ + Use: "set-last-context", + Short: "Switch to the last used context from the history", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + stores, config, err := initialize() + if err != nil { + return err + } + + kc, err := history.SetLastContext(stores, config, stateDirectory, noIndex) + reportNewContext(kc) + return err + }, + } + + listContextsCmd = &cobra.Command{ + Use: "list-contexts", + Short: "List all available contexts without fuzzy search", + Aliases: []string{"ls"}, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + lc, err := listContexts("") + if err != nil { + return err + } + for _, c := range lc { + fmt.Println(c) + } + return nil + }, + } + + setContextCmd = &cobra.Command{ + Use: "set-context", + Short: "Switch to context name provided as first argument", + Long: `Switch to context name provided as first argument. KubeContext name has to exist in any of the found Kubeconfig files.`, + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + log := logrus.New().WithField("hook", "") + return hooks.Hooks(log, configPath, stateDirectory, "", false) + }, + RunE: func(cmd *cobra.Command, args []string) error { + stores, config, err := initialize() + if err != nil { + return err + } + + kc, err := set_context.SetContext(args[0], stores, config, stateDirectory, noIndex, true) + reportNewContext(kc) + return err + }, + SilenceUsage: true, + } + + deleteContextCmd = &cobra.Command{ + Use: "delete-context", + Short: "Delete context name provided as first argument", + Long: `Delete context name provided as first argument. KubeContext name has to exist in the current Kubeconfig file.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctxName, err := resolveContextName(args[0]) + fmt.Println("delete-context", ctxName, args, err) + if err != nil { + return err + } + return delete_context.DeleteContext(ctxName) + }, + } + + unsetContextCmd = &cobra.Command{ + Use: "unset-context", + Short: "Unset current-context", + Long: `Unset current-context in the current Kubeconfig file.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return unset_context.UnsetCurrentContext() + }, + } + + currentContextCmd = &cobra.Command{ + Use: "current-context", + Short: "Show current-context", + Long: `Show current-context in the current Kubeconfig file.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, err := util.GetCurrentContext() + if err != nil { + return err + } + fmt.Println(ctx) + return nil + }, + } +) + +func init() { + rootCommand.AddCommand(currentContextCmd) + rootCommand.AddCommand(deleteContextCmd) + rootCommand.AddCommand(setContextCmd) + rootCommand.AddCommand(listContextsCmd) + rootCommand.AddCommand(unsetContextCmd) + rootCommand.AddCommand(previousContextCmd) + rootCommand.AddCommand(lastContextCmd) + + setFlagsForContextCommands(setContextCmd) + setFlagsForContextCommands(listContextsCmd) + // need to add flags as the namespace history allows switching to any {context: namespace} combination + setFlagsForContextCommands(previousContextCmd) + setFlagsForContextCommands(lastContextCmd) +} + +func listContexts(prefix string) ([]string, error) { + stores, config, err := initialize() + if err != nil { + return nil, err + } + + lc, err := list_contexts.ListContexts(stores, config, stateDirectory, noIndex, prefix) + if err != nil { + return nil, err + } + return lc, nil +} + +func resolveContextName(contextName string) (string, error) { + if contextName == "." { + c, err := util.GetCurrentContext() + if err != nil { + return "", err + } + contextName = c + } + return contextName, nil +} + +func setFlagsForContextCommands(command *cobra.Command) { + setCommonFlags(command) + command.Flags().StringVar( + &storageBackend, + "store", + "filesystem", + "the backing store to be searched for kubeconfig files. Can be either \"filesystem\" or \"vault\"") + command.Flags().StringVar( + &kubeconfigName, + "kubeconfig-name", + defaultKubeconfigName, + "only shows kubeconfig files with this name. Accepts wilcard arguments '*' and '?'. Defaults to 'config'.") + command.Flags().StringVar( + &vaultAPIAddressFromFlag, + "vault-api-address", + "", + "the API address of the Vault store. Overrides the default \"vaultAPIAddress\" field in the SwitchConfig. This flag is overridden by the environment variable \"VAULT_ADDR\".") + command.Flags().StringVar( + &configPath, + "config-path", + os.ExpandEnv("$HOME/.kube/switch-config.yaml"), + "path on the local filesystem to the configuration file.") + // not used for setContext command. Makes call in switch.sh script easier (no need to exclude flag from call) + command.Flags().BoolVar( + &showPreview, + "show-preview", + true, + "show preview of the selected kubeconfig. Possibly makes sense to disable when using vault as the kubeconfig store to prevent excessive requests against the API.") +} + +func reportNewContext(ctxName *string) { + if ctxName == nil { + return + } + fmt.Printf("switched to context \"%s\".\n", *ctxName) +} diff --git a/cmd/switcher/gardener.go b/cmd/switcher/gardener.go new file mode 100644 index 000000000..46a5c4a9d --- /dev/null +++ b/cmd/switcher/gardener.go @@ -0,0 +1,43 @@ +package switcher + +import ( + "os" + + gardenercontrolplane "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/gardener" + "github.com/spf13/cobra" +) + +var ( + gardenerCmd = &cobra.Command{ + Use: "gardener", + Short: "gardener specific commands", + Long: `Commands that can only be used if a Gardener store is configured.`, + } + + controlplaneCmd = &cobra.Command{ + Use: "controlplane", + Short: "Switch to the Shoot's controlplane", + RunE: func(cmd *cobra.Command, args []string) error { + stores, _, err := initialize() + if err != nil { + return err + } + + _, err = gardenercontrolplane.SwitchToControlplane(stores, getKubeconfigPathFromFlag()) + return err + }, + } +) + +func init() { + setCommonFlags(controlplaneCmd) + controlplaneCmd.Flags().StringVar( + &configPath, + "config-path", + os.ExpandEnv("$HOME/.kube/switch-config.yaml"), + "path on the local filesystem to the configuration file.") + + gardenerCmd.AddCommand(controlplaneCmd) + + rootCommand.AddCommand(gardenerCmd) +} diff --git a/cmd/switcher/history.go b/cmd/switcher/history.go new file mode 100644 index 000000000..2dd5450b2 --- /dev/null +++ b/cmd/switcher/history.go @@ -0,0 +1,30 @@ +package switcher + +import ( + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/history" + "github.com/spf13/cobra" +) + +var ( + historyCmd = &cobra.Command{ + Use: "history", + Aliases: []string{"h"}, + Short: "Switch to any previous tuple {context,namespace} from the history", + Long: `Lists the context history with the ability to switch to a previous context.`, + RunE: func(cmd *cobra.Command, args []string) error { + stores, config, err := initialize() + if err != nil { + return err + } + + kc, err := history.SwitchToHistory(stores, config, stateDirectory, noIndex) + reportNewContext(kc) + return err + }, + } +) + +func init() { + setFlagsForContextCommands(historyCmd) + rootCommand.AddCommand(historyCmd) +} diff --git a/cmd/switcher/hooks.go b/cmd/switcher/hooks.go new file mode 100644 index 000000000..601e4724c --- /dev/null +++ b/cmd/switcher/hooks.go @@ -0,0 +1,71 @@ +package switcher + +import ( + "os" + + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/hooks" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var ( + hookCmd = &cobra.Command{ + Use: "hooks", + Short: "Run configured hooks", + RunE: func(cmd *cobra.Command, args []string) error { + log := logrus.New().WithField("hook", hookName) + return hooks.Hooks(log, configPath, stateDirectory, hookName, runImmediately) + }, + } + + hookLsCmd = &cobra.Command{ + Use: "ls", + Short: "List configured hooks", + RunE: func(cmd *cobra.Command, args []string) error { + log := logrus.New().WithField("hook-ls", hookName) + return hooks.ListHooks(log, configPath, stateDirectory) + }, + } +) + +func init() { + hookLsCmd.Flags().StringVar( + &configPath, + "config-path", + os.ExpandEnv("$HOME/.kube/switch-config.yaml"), + "path on the local filesystem to the configuration file.") + + hookLsCmd.Flags().StringVar( + &stateDirectory, + "state-directory", + os.ExpandEnv("$HOME/.kube/switch-state"), + "path to the state directory.") + + hookCmd.AddCommand(hookLsCmd) + + hookCmd.Flags().StringVar( + &configPath, + "config-path", + os.ExpandEnv("$HOME/.kube/switch-config.yaml"), + "path on the local filesystem to the configuration file.") + + hookCmd.Flags().StringVar( + &stateDirectory, + "state-directory", + os.ExpandEnv("$HOME/.kube/switch-state"), + "path to the state directory.") + + hookCmd.Flags().StringVar( + &hookName, + "hook-name", + "", + "the name of the hook that should be run.") + + hookCmd.Flags().BoolVar( + &runImmediately, + "run-immediately", + true, + "run hooks right away. Do not respect the hooks execution configuration.") + + rootCommand.AddCommand(hookCmd) +} diff --git a/cmd/switcher/namespace.go b/cmd/switcher/namespace.go new file mode 100644 index 000000000..02144af58 --- /dev/null +++ b/cmd/switcher/namespace.go @@ -0,0 +1,28 @@ +package switcher + +import ( + "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/ns" + "github.com/spf13/cobra" +) + +var ( + namespaceCommand = &cobra.Command{ + Use: "namespace", + Aliases: []string{"ns"}, + Short: "Change the current namespace", + Long: `Search namespaces in the current cluster and change to it.`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 && len(args[0]) > 0 { + return ns.SwitchToNamespace(args[0], getKubeconfigPathFromFlag()) + } + + return ns.SwitchNamespace(getKubeconfigPathFromFlag(), stateDirectory, noIndex) + }, + SilenceErrors: true, + } +) + +func init() { + setCommonFlags(namespaceCommand) + rootCommand.AddCommand(namespaceCommand) +} diff --git a/cmd/switcher/switcher.go b/cmd/switcher/switcher.go index 519538527..07bb8dd9b 100644 --- a/cmd/switcher/switcher.go +++ b/cmd/switcher/switcher.go @@ -17,27 +17,19 @@ package switcher import ( "fmt" "os" - "runtime" "strings" + "github.com/danielfoehrkn/kubeswitch/pkg" + "github.com/danielfoehrkn/kubeswitch/pkg/cache" - gardenercontrolplane "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/gardener" - "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/history" - "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/ns" "github.com/danielfoehrkn/kubeswitch/pkg/util" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "k8s.io/utils/pointer" - "github.com/danielfoehrkn/kubeswitch/pkg" switchconfig "github.com/danielfoehrkn/kubeswitch/pkg/config" "github.com/danielfoehrkn/kubeswitch/pkg/config/validation" "github.com/danielfoehrkn/kubeswitch/pkg/store" - "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/alias" - "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/clean" - "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/hooks" - list_contexts "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/list-contexts" - setcontext "github.com/danielfoehrkn/kubeswitch/pkg/subcommands/set-context" "github.com/danielfoehrkn/kubeswitch/types" ) @@ -53,6 +45,9 @@ var ( kubeconfigPath string kubeconfigName string showPreview bool + deleteContext bool + unsetContext bool + currentContext bool // vault store storageBackend string @@ -72,331 +67,75 @@ var ( noIndex bool rootCommand = &cobra.Command{ - Use: "switch", + Use: "switcher", Short: "Launch the switch binary", Long: `The kubectx for operators.`, Version: version, - RunE: func(cmd *cobra.Command, args []string) error { - stores, config, err := initialize() - if err != nil { - return err - } - - // config file setting overwrites the command line default (--showPreview true) - if showPreview && config.ShowPreview != nil && !*config.ShowPreview { - showPreview = false - } - - return pkg.Switcher(stores, config, stateDirectory, noIndex, showPreview) - }, - } -) - -func init() { - aliasContextCmd := &cobra.Command{ - Use: "alias", - Short: "Create an alias for a context. Use ALIAS=CONTEXT_NAME", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 || !strings.Contains(args[0], "=") || len(strings.Split(args[0], "=")) != 2 { - return fmt.Errorf("please provide the alias in the form ALIAS=CONTEXT_NAME") - } - arguments := strings.Split(args[0], "=") - - stores, config, err := initialize() - if err != nil { - return err - } - - return alias.Alias(arguments[0], arguments[1], stores, config, stateDirectory, noIndex) - }, - } - - aliasLsCmd := &cobra.Command{ - Use: "ls", - Short: "List all existing aliases", - RunE: func(cmd *cobra.Command, args []string) error { - return alias.ListAliases(stateDirectory) - }, - } - - aliasRmCmd := &cobra.Command{ - Use: "rm", - Short: "Remove an existing alias", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 || len(args[0]) == 0 { - return fmt.Errorf("please provide the alias to remove as the first argument") - } - - return alias.RemoveAlias(args[0], stateDirectory) - }, - } - - aliasRmCmd.Flags().StringVar( - &stateDirectory, - "state-directory", - os.ExpandEnv("$HOME/.kube/switch-state"), - "path to the state directory.") - - aliasContextCmd.AddCommand(aliasLsCmd) - aliasContextCmd.AddCommand(aliasRmCmd) - - previousContextCmd := &cobra.Command{ - Use: "set-previous-context", - Short: "Switch to the previous context from the history", - RunE: func(cmd *cobra.Command, args []string) error { - stores, config, err := initialize() - if err != nil { - return err - } - - return history.SetPreviousContext(stores, config, stateDirectory, noIndex) - }, - } - - lastContextCmd := &cobra.Command{ - Use: "set-last-context", - Short: "Switch to the last used context from the history", - RunE: func(cmd *cobra.Command, args []string) error { - stores, config, err := initialize() - if err != nil { - return err + Args: func(cmd *cobra.Command, args []string) error { + switch { + case deleteContext: + if err := cobra.ExactArgs(1)(cmd, args); err != nil { + return err + } + case unsetContext || currentContext: + if err := cobra.NoArgs(cmd, args); err != nil { + return err + } } - - return history.SetLastContext(stores, config, stateDirectory, noIndex) + return cmd.ParseFlags(args) }, - } - - historyCmd := &cobra.Command{ - Use: "history", - Aliases: []string{"h"}, - Short: "Switch to any previous tuple {context,namespace} from the history", - Long: `Lists the context history with the ability to switch to a previous context.`, RunE: func(cmd *cobra.Command, args []string) error { - stores, config, err := initialize() - if err != nil { - return err + switch { + case deleteContext: + return deleteContextCmd.RunE(cmd, args) + case unsetContext: + return unsetContextCmd.RunE(cmd, args) + case currentContext: + return currentContextCmd.RunE(cmd, args) } - return history.SwitchToHistory(stores, config, stateDirectory, noIndex) - }, - } - - namespaceCommand := &cobra.Command{ - Use: "namespace", - Aliases: []string{"ns"}, - Short: "Change the current namespace", - Long: `Search namespaces in the current cluster and change to it.`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 1 && len(args[0]) > 0 { - return ns.SwitchToNamespace(args[0], getKubeconfigPathFromFlag()) + if len(args) > 0 { + switch args[0] { + case "-": + return previousContextCmd.RunE(cmd, args[1:]) + case ".": + return lastContextCmd.RunE(cmd, args[1:]) + default: + return setContextCmd.RunE(cmd, args) + } } - return ns.SwitchNamespace(getKubeconfigPathFromFlag(), stateDirectory, noIndex) - }, - } - - setContextCmd := &cobra.Command{ - Use: "set-context", - Short: "Switch to context name provided as first argument", - Long: `Switch to context name provided as first argument. KubeContext name has to exist in any of the found Kubeconfig files.`, - RunE: func(cmd *cobra.Command, args []string) error { stores, config, err := initialize() if err != nil { return err } - _, err = setcontext.SetContext(args[0], stores, config, stateDirectory, noIndex, true) - return err - }, - } - - gardenerCmd := &cobra.Command{ - Use: "gardener", - Short: "gardener specific commands", - Long: `Commands that can only be used if a Gardener store is configured.`, - } - - controlplaneCmd := &cobra.Command{ - Use: "controlplane", - Short: "Switch to the Shoot's controlplane", - RunE: func(cmd *cobra.Command, args []string) error { - stores, _, err := initialize() - if err != nil { - return err + // config file setting overwrites the command line default (--showPreview true) + if showPreview && config.ShowPreview != nil && !*config.ShowPreview { + showPreview = false } - _, err = gardenercontrolplane.SwitchToControlplane(stores, getKubeconfigPathFromFlag()) + kc, err := pkg.Switcher(stores, config, stateDirectory, noIndex, showPreview) + reportNewContext(kc) return err }, - } - - gardenerCmd.AddCommand(controlplaneCmd) - - listContextsCmd := &cobra.Command{ - Use: "list-contexts", - Short: "List all available contexts without fuzzy search", - Aliases: []string{"ls"}, - RunE: func(cmd *cobra.Command, args []string) error { - stores, config, err := initialize() - if err != nil { - return err - } - - return list_contexts.ListContexts(stores, config, stateDirectory, noIndex) + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + lc, _ := listContexts(toComplete) + return lc, cobra.ShellCompDirectiveNoFileComp }, + SilenceUsage: true, } - - cleanCmd := &cobra.Command{ - Use: "clean", - Short: "Cleans all temporary and cached kubeconfig files", - Long: `Cleans the temporary kubeconfig files created in the directory $HOME/.kube/switch_tmp and flushes every cache`, - RunE: func(cmd *cobra.Command, args []string) error { - stores, _, err := initialize() - if err != nil { - return err - } - return clean.Clean(stores) - }, - } - - hookCmd := &cobra.Command{ - Use: "hooks", - Short: "Run configured hooks", - RunE: func(cmd *cobra.Command, args []string) error { - log := logrus.New().WithField("hook", hookName) - return hooks.Hooks(log, configPath, stateDirectory, hookName, runImmediately) - }, - } - - hookLsCmd := &cobra.Command{ - Use: "ls", - Short: "List configured hooks", - RunE: func(cmd *cobra.Command, args []string) error { - log := logrus.New().WithField("hook-ls", hookName) - return hooks.ListHooks(log, configPath, stateDirectory) - }, - } - hookLsCmd.Flags().StringVar( - &configPath, - "config-path", - os.ExpandEnv("$HOME/.kube/switch-config.yaml"), - "path on the local filesystem to the configuration file.") - - hookLsCmd.Flags().StringVar( - &stateDirectory, - "state-directory", - os.ExpandEnv("$HOME/.kube/switch-state"), - "path to the state directory.") - - hookCmd.AddCommand(hookLsCmd) - - hookCmd.Flags().StringVar( - &configPath, - "config-path", - os.ExpandEnv("$HOME/.kube/switch-config.yaml"), - "path on the local filesystem to the configuration file.") - - hookCmd.Flags().StringVar( - &stateDirectory, - "state-directory", - os.ExpandEnv("$HOME/.kube/switch-state"), - "path to the state directory.") - - hookCmd.Flags().StringVar( - &hookName, - "hook-name", - "", - "the name of the hook that should be run.") - - hookCmd.Flags().BoolVar( - &runImmediately, - "run-immediately", - true, - "run hooks right away. Do not respect the hooks execution configuration.") - - versionCmd := &cobra.Command{ - Use: "version", - Short: "show Switch Version info", - Long: "show the Switch version information", - Example: "switch version", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Printf(`Switch: - version : %s - build date : %s - go version : %s - go compiler : %s - platform : %s/%s -`, version, buildDate, runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH) - - return nil - }, - } - rootCommand.AddCommand(setContextCmd) - rootCommand.AddCommand(listContextsCmd) - rootCommand.AddCommand(cleanCmd) - rootCommand.AddCommand(namespaceCommand) - rootCommand.AddCommand(hookCmd) - rootCommand.AddCommand(historyCmd) - rootCommand.AddCommand(previousContextCmd) - rootCommand.AddCommand(lastContextCmd) - rootCommand.AddCommand(aliasContextCmd) - rootCommand.AddCommand(versionCmd) - rootCommand.AddCommand(gardenerCmd) - - setContextCmd.SilenceUsage = true - aliasContextCmd.SilenceErrors = true - aliasRmCmd.SilenceErrors = true - namespaceCommand.SilenceErrors = true - - setFlagsForContextCommands(setContextCmd) - setFlagsForContextCommands(listContextsCmd) - setFlagsForContextCommands(historyCmd) - // need to add flags as the namespace history allows switching to any {context: namespace} combination - setFlagsForContextCommands(previousContextCmd) - setFlagsForContextCommands(lastContextCmd) - setFlagsForContextCommands(aliasContextCmd) - - setCommonFlags(namespaceCommand) - setControlplaneCommandFlags(controlplaneCmd) -} - -func NewCommandStartSwitcher() *cobra.Command { - return rootCommand -} +) func init() { setFlagsForContextCommands(rootCommand) - rootCommand.SilenceUsage = true + rootCommand.Flags().BoolVarP(&deleteContext, "d", "d", false, "delete desired context. Context name is required") + rootCommand.Flags().BoolVarP(&unsetContext, "unset", "u", false, "unset current context") + rootCommand.Flags().BoolVarP(¤tContext, "current", "c", false, "show current context") } -func setFlagsForContextCommands(command *cobra.Command) { - setCommonFlags(command) - command.Flags().StringVar( - &storageBackend, - "store", - "filesystem", - "the backing store to be searched for kubeconfig files. Can be either \"filesystem\" or \"vault\"") - command.Flags().StringVar( - &kubeconfigName, - "kubeconfig-name", - defaultKubeconfigName, - "only shows kubeconfig files with this name. Accepts wilcard arguments '*' and '?'. Defaults to 'config'.") - command.Flags().StringVar( - &vaultAPIAddressFromFlag, - "vault-api-address", - "", - "the API address of the Vault store. Overrides the default \"vaultAPIAddress\" field in the SwitchConfig. This flag is overridden by the environment variable \"VAULT_ADDR\".") - command.Flags().StringVar( - &configPath, - "config-path", - os.ExpandEnv("$HOME/.kube/switch-config.yaml"), - "path on the local filesystem to the configuration file.") - // not used for setContext command. Makes call in switch.sh script easier (no need to exclude flag from call) - command.Flags().BoolVar( - &showPreview, - "show-preview", - true, - "show preview of the selected kubeconfig. Possibly makes sense to disable when using vault as the kubeconfig store to prevent excessive requests against the API.") +func NewCommandStartSwitcher() *cobra.Command { + return rootCommand } func setCommonFlags(command *cobra.Command) { @@ -422,15 +161,6 @@ func setCommonFlags(command *cobra.Command) { "path to the local directory used for storing internal state.") } -func setControlplaneCommandFlags(cmd *cobra.Command) { - setCommonFlags(cmd) - cmd.Flags().StringVar( - &configPath, - "config-path", - os.ExpandEnv("$HOME/.kube/switch-config.yaml"), - "path on the local filesystem to the configuration file.") -} - func initialize() ([]store.KubeconfigStore, *types.Config, error) { if showDebugLogs { logrus.SetLevel(logrus.DebugLevel) diff --git a/cmd/switcher/version.go b/cmd/switcher/version.go new file mode 100644 index 000000000..aea3b365a --- /dev/null +++ b/cmd/switcher/version.go @@ -0,0 +1,32 @@ +package switcher + +import ( + "fmt" + "runtime" + + "github.com/spf13/cobra" +) + +var ( + versionCmd = &cobra.Command{ + Use: "version", + Short: "show Switch Version info", + Long: "show the Switch version information", + Example: "switch version", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Printf(`Switch: + version : %s + build date : %s + go version : %s + go compiler : %s + platform : %s/%s +`, version, buildDate, runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH) + + return nil + }, + } +) + +func init() { + rootCommand.AddCommand(versionCmd) +} diff --git a/docs/command_completion.md b/docs/command_completion.md index 98a878771..50236f407 100644 --- a/docs/command_completion.md +++ b/docs/command_completion.md @@ -3,21 +3,18 @@ Currently, command line completion is not pre-installed in any installation method. You need to do it manually. -## Bash +Install the completion script by running: -Source this [this script ](../scripts/_switch.bash) from your `~/.bashrc` -or put it into [your completions directory](https://serverfault.com/questions/506612/standard-place-for-user-defined-bash-completion-d-scripts). - -## Zsh - -There is currently only a bash completion script. -But you can use it in zsh also. - -Add below lines to your `~/.zshrc` file (before you source the bash completion script). +### Bash +```sh +echo 'source <(switch completion bash)' >> ~/.bashrc ``` -autoload bashcompinit -bashcompinit +### Zsh +```sh +echo 'source <(switch completion zsh)' >> ~/.bashrc ``` - -Then source the bash completion script. \ No newline at end of file +### Fish +```sh +echo 'kubeswitch completion fish | source' >> ~/.config/fish/config.fish +``` \ No newline at end of file diff --git a/hack/switch/switch.fish b/hack/switch/switch.fish new file mode 100755 index 000000000..12df379c2 --- /dev/null +++ b/hack/switch/switch.fish @@ -0,0 +1,50 @@ +#!/usr/bin/env fish + +function kubeswitch +# if the executable path is not set, the switcher binary has to be on the path +# this is the case when installing it via homebrew + set -f DEFAULT_EXECUTABLE_PATH 'switcher' + set -f REPORT_RESPONSE + set -f opts + + for i in $argv + switch "$i" + case --executable-path + set -f EXECUTABLE_PATH $i + case completion + set -a opts $i --cmd kubeswitch + case '*' + set -a opts $i + end + end + + if test -z "$EXECUTABLE_PATH" + set -f EXECUTABLE_PATH $DEFAULT_EXECUTABLE_PATH + end + + set -f RESULT 0 + set -f RESPONSE ($EXECUTABLE_PATH $opts; or set RESULT $status | string split0) + if test $RESULT -ne 0; or test -z "$RESPONSE" + printf "%s\n" $RESPONSE + return $RESULT + end + + set -l trim_left "switched to context \"" + set -l trim_right "\"." + if string match -q "$trim_left*$trim_right" -- "$RESPONSE" + set -l new_config (string replace -r "$trim_left(.*)$trim_right\$" '$1' -- "$RESPONSE") + + if test ! -e "$new_config" + echo "ERROR: \"$new_config\" does not exist" + return 1 + end + + set -l switchTmpDirectory "$HOME/.kube/.switch_tmp/config" + if test -n "$KUBECONFIG"; and string match -q "*$switchTmpDirectory*" -- "$KUBECONFIG" + rm -f "$KUBECONFIG" + end + + set -gx KUBECONFIG "$new_config" + end + printf "%s\n" $RESPONSE +end diff --git a/hack/switch/switch.sh b/hack/switch/switch.sh index 775c9cbe2..ce416166e 100755 --- a/hack/switch/switch.sh +++ b/hack/switch/switch.sh @@ -1,591 +1,60 @@ #!/usr/bin/env bash -hooksUsage() { -echo ' -Run configured hooks - -Usage: - switch hooks [flags] - switch hooks [command] - -Available Commands: - ls List configured hooks - -Flags: - --config-path string path on the local filesystem to the configuration file. (default "~/.kube/switch-config.yaml") - -h, --help help for hooks - --hook-name string the name of the hook that should be run. - --run-immediately run hooks right away. Do not respect the hooks execution configuration. (default true) - --state-directory string path to the state directory. (default "~/.kube/switch-state") -' -} - -gardenerUsage() { -echo ' -Commands that can only be used if a Gardener store is configured. - -Usage: - switch gardener [command] - -Available Commands: - controlplane Switch to the Shoots controlplane - -Flags: - -h, --help help for gardener -' -} - -aliasUsage() { -echo ' -Create an alias for a context. - -Usage: - switch alias ALIAS=CONTEXT_NAME - switch alias [flags] - switch alias [command] - -Available Commands: - ls List all existing aliases - rm Remove an existing alias - -Flags: - --config-path string path on the local filesystem to the configuration file. (default "~/.kube/switch-config.yaml") - -h, --help help for alias - --kubeconfig-name string only shows kubeconfig files with this name. Accepts wilcard arguments "*" and "?". Defaults to "config". (default "config") - --kubeconfig-path string path to be recursively searched for kubeconfig files. Can be a file or a directory on the local filesystem or a path in Vault. (default "~/.kube/config") - --state-directory string path to the local directory used for storing internal state. (default "~/.kube/switch-state") - --store string the backing store to be searched for kubeconfig files. Can be either "filesystem" or "vault" (default "filesystem") - --vault-api-address string the API address of the Vault store. Overrides the default "vaultAPIAddress" field in the SwitchConfig. This flag is overridden by the environment variable "VAULT_ADDR". -' -} - -switchUsage() { -echo ' -The kubectx for operators. - -Usage: - switch [flags] - switch [command] - -Available Commands: - alias Create an alias for a context. Use ALIAS=CONTEXT_NAME - clean Cleans all temporary and cached kubeconfig files - completion Generate the autocompletion script for the specified shell - gardener gardener specific commands - help Help about any command - history Switch to any previous tuple {context,namespace} from the history - hooks Run configured hooks - list-contexts List all available contexts without fuzzy search - namespace Change the current namespace - set-context Switch to context name provided as first argument - set-last-context Switch to the last used context from the history - set-previous-context Switch to the previous context from the history - version show Switch Version info - -Flags: - --config-path string path on the local filesystem to the configuration file. (default "/Users/d060239/.kube/switch-config.yaml") - --debug show debug logs - -h, --help help for switch - --kubeconfig-name string only shows kubeconfig files with this name. Accepts wilcard arguments "*" and "?". Defaults to "config". (default "config") - --kubeconfig-path string path to be recursively searched for kubeconfigs. Can be a file or a directory on the local filesystem or a path in Vault. (default "$HOME/.kube/config") - --no-index stores do not read from index files. The index is refreshed. - --show-preview show preview of the selected kubeconfig. Possibly makes sense to disable when using vault as the kubeconfig store to prevent excessive requests against the API. (default true) - --state-directory string path to the local directory used for storing internal state. (default "/Users/d060239/.kube/switch-state") - --store string the backing store to be searched for kubeconfig files. Can be either "filesystem" or "vault" (default "filesystem") - --vault-api-address string the API address of the Vault store. Overrides the default "vaultAPIAddress" field in the SwitchConfig. This flag is overridden by the environment variable "VAULT_ADDR". - -v, --version version for switch - -' -} - -unknownCommand() { - echo "error: unknown command \"$1\" for "switch" -Run 'switch --help' for usage." -} - -usage() -{ - case "$1" in - alias) - aliasUsage - return - ;; - hooks) - hooksUsage - return - ;; - gardener) - gardenerUsage - return - ;; - *) - switchUsage - return - ;; - esac - -} - function switch(){ # if the executable path is not set, the switcher binary has to be on the path # this is the case when installing it via homebrew - DEFAULT_EXECUTABLE_PATH='switcher' - - KUBECONFIG_PATH='' - STORE='' - KUBECONFIG_NAME='' - SHOW_PREVIEW='' - CONFIG_PATH='' - VAULT_API_ADDRESS='' - EXECUTABLE_PATH='' - CLEAN='' - SET_CONTEXT='' - NAMESPACE='' - NAMESPACE_ARGUMENT='' - HISTORY='' - PREV_HISTORY='' - LAST_HISTORY='' - LIST_CONTEXTS='' - CURRENT_CONTEXT='' - ALIAS='' - ALIAS_ARGUMENTS='' - ALIAS_ARGUMENTS_ALIAS='' - GARDENER='' - GARDENER_ARGUMENT='' - UNSET_CURRENT_CONTEXT='' - DELETE_CONTEXT='' - VERSION='' - DEBUG='' - NO_INDEX='' - # Hooks - HOOKS='' - HOOKS_ARGUMENTS='' - STATE_DIRECTORY='' - HOOK_NAME='' - RUN_IMMEDIATELY='' + local DEFAULT_EXECUTABLE_PATH="switcher" + declare -a opts while test $# -gt 0; do - case "$1" in - --kubeconfig-path) - shift - KUBECONFIG_PATH=$1 - shift - ;; - --store) - shift - STORE=$1 - shift - ;; - --kubeconfig-name) - shift - KUBECONFIG_NAME=$1 - shift - ;; - --show-preview) - shift - SHOW_PREVIEW=$1 - shift - ;; - --vault-api-address) - shift - VAULT_API_ADDRESS=$1 - shift - ;; - --executable-path) - shift - EXECUTABLE_PATH=$1 - shift - ;; - -c) - CURRENT_CONTEXT=$1 - shift - ;; - --current) - CURRENT_CONTEXT=$1 - shift - ;; - clean) - CLEAN=$1 - shift - ;; - h) - HISTORY=$1 - shift - ;; - ns) - NAMESPACE=$1 - NAMESPACE_ARGUMENT=$2 - shift - ;; - namespace) - NAMESPACE=$1 - NAMESPACE_ARGUMENT=$2 - shift - ;; - history) - HISTORY=$1 - shift - ;; - -) - PREV_HISTORY=$1 - shift - ;; - .) - LAST_HISTORY=$1 - shift - ;; - -u) - UNSET_CURRENT_CONTEXT=$1 - shift - ;; - --unset) - UNSET_CURRENT_CONTEXT=$1 - shift - ;; - -d) - shift - DELETE_CONTEXT=$1 - shift - ;; - list-contexts) - LIST_CONTEXTS=$1 - shift - ;; - hooks) - HOOKS=$1 - HOOKS_ARGUMENTS=$2 - shift - ;; - gardener) - GARDENER=$1 - GARDENER_ARGUMENT=$2 - shift - ;; - alias) - ALIAS=$1 - ALIAS_ARGUMENTS=$2 - ALIAS_ARGUMENTS_ALIAS=$3 - shift - ;; - --state-directory) - shift - STATE_DIRECTORY=$1 - shift - ;; - --debug) - DEBUG=$1 - shift - ;; - --no-index) - NO_INDEX=$1 - shift - ;; - --config-path) - shift - CONFIG_PATH=$1 - shift - ;; - --hook-name) - shift - HOOK_NAME=$1 - shift - ;; - --run-hooks-immediately) - shift - RUN_IMMEDIATELY=$1 - shift - ;; - --help) - usage $HOOKS $ALIAS $GARDENER - return - ;; - -h) - usage $HOOKS $ALIAS $GARDENER - return - ;; - version) - VERSION=$1 - shift - ;; - -v) - VERSION=$1 - shift - ;; - *) - SET_CONTEXT=$1 - shift - ;; - esac - done - - - if [ -n "$UNSET_CURRENT_CONTEXT" ] - then - kubectl config unset current-context - return - fi - - if [ -n "$DELETE_CONTEXT" ] - then - case $DELETE_CONTEXT in - .) - kubectl config delete-context $(kubectl config current-context) - ;; - *) - kubectl config delete-context $DELETE_CONTEXT - ;; - esac - return - fi - - if [ -n "$CURRENT_CONTEXT" ] - then - kubectl config current-context - return - fi + case "$1" in + --executable-path) + EXECUTABLE_PATH="$1" + ;; + completion) + opts+=("$1" --cmd switch) + ;; + *) + opts+=( "$1" ) + ;; + esac + shift + done if [ -z "$EXECUTABLE_PATH" ] then - EXECUTABLE_PATH=$DEFAULT_EXECUTABLE_PATH - fi - - if [ -n "$VERSION" ] - then - $EXECUTABLE_PATH version - return + EXECUTABLE_PATH="$DEFAULT_EXECUTABLE_PATH" fi - DEBUG_FLAG='' - if [ -n "$DEBUG" ] + RESPONSE="$($EXECUTABLE_PATH "${opts[@]}")" + if [ $? -ne 0 -o -z "$RESPONSE" ] then - DEBUG="$DEBUG" - DEBUG_FLAG=--debug + printf "%s\n" "$RESPONSE" + return $? fi - if [ -n "$ALIAS" ] + local trim_left="switched to context \"" + local trim_right="\"." + if [[ "$RESPONSE" == "$trim_left"*"$trim_right" ]] then - # for switch alias rm - if [ -n "$ALIAS_ARGUMENTS_ALIAS" ]; then - $EXECUTABLE_PATH alias "$ALIAS_ARGUMENTS" "$ALIAS_ARGUMENTS_ALIAS" - return - fi + local new_config="${RESPONSE#$trim_left}" + new_config="${new_config%$trim_right}" - # compatibility with kubectx =. rename current-context to - if [[ "$ALIAS_ARGUMENTS" == *=. ]]; then - lastCharRemoved=${ALIAS_ARGUMENTS: : -1} - currentContextAlias=$lastCharRemoved$(kubectl config current-context) - $EXECUTABLE_PATH alias "$currentContextAlias" \ - $DEBUG_FLAG ${DEBUG} - return - fi - - $EXECUTABLE_PATH alias "$ALIAS_ARGUMENTS" \ - $DEBUG_FLAG ${DEBUG} - return - fi - - if [ -n "$CLEAN" ] - then - $EXECUTABLE_PATH clean - return - fi - - if [ -n "$PREV_HISTORY" ] - then - NEW_KUBECONFIG=$($EXECUTABLE_PATH -) - setKubeconfigEnvironmentVariable $NEW_KUBECONFIG - return - fi - - if [ -n "$LAST_HISTORY" ] - then - NEW_KUBECONFIG=$($EXECUTABLE_PATH .) - setKubeconfigEnvironmentVariable $NEW_KUBECONFIG - return - fi - - if [ -n "$LIST_CONTEXTS" ] - then - $EXECUTABLE_PATH list-contexts - return - fi - - KUBECONFIG_PATH_FLAG='' - if [ -n "$KUBECONFIG_PATH" ] - then - KUBECONFIG_PATH="$KUBECONFIG_PATH" - KUBECONFIG_PATH_FLAG=--kubeconfig-path - fi - - STORE_FLAG='' - if [ -n "$STORE" ] - then - STORE="$STORE" - STORE_FLAG=--store - fi - - KUBECONFIG_NAME_FLAG='' - if [ -n "$KUBECONFIG_NAME" ] - then - KUBECONFIG_NAME="$KUBECONFIG_NAME" - KUBECONFIG_NAME_FLAG=--kubeconfig-name - fi - - SHOW_PREVIEW_FLAG=--show-preview - if [ -n "$SHOW_PREVIEW" ] - then - SHOW_PREVIEW="$SHOW_PREVIEW" - else - SHOW_PREVIEW="true" - fi - - VAULT_API_ADDRESS_FLAG='' - if [ -n "$VAULT_API_ADDRESS" ] - then - VAULT_API_ADDRESS="$VAULT_API_ADDRESS" - VAULT_API_ADDRESS_FLAG=--vault-api-address - fi - - STATE_DIRECTORY_FLAG='' - if [ -n "$STATE_DIRECTORY" ] - then - STATE_DIRECTORY="$STATE_DIRECTORY" - STATE_DIRECTORY_FLAG=--state-directory - fi - - CONFIG_PATH_FLAG='' - if [ -n "$CONFIG_PATH" ] - then - CONFIG_PATH="$CONFIG_PATH" - CONFIG_PATH_FLAG=--config-path - fi - - NO_INDEX_FLAG='' - if [ -n "$NO_INDEX" ] - then - NO_INDEX="$NO_INDEX" - NO_INDEX_FLAG=--no-index - fi - - if [ -n "$SET_CONTEXT" ] - then - SET_CONTEXT="$SET_CONTEXT" - fi - - if [ -n "$HISTORY" ] - then - NEW_KUBECONFIG=$($EXECUTABLE_PATH history \ - $KUBECONFIG_PATH_FLAG ${KUBECONFIG_PATH} \ - $STORE_FLAG ${STORE} \ - $KUBECONFIG_NAME_FLAG ${KUBECONFIG_NAME} \ - $SHOW_PREVIEW_FLAG=${SHOW_PREVIEW} \ - $VAULT_API_ADDRESS_FLAG ${VAULT_API_ADDRESS} \ - $DEBUG_FLAG ${DEBUG} \ - $NO_INDEX_FLAG ${NO_INDEX} \ - ) - - setKubeconfigEnvironmentVariable $NEW_KUBECONFIG - return - fi - - if [ -n "$GARDENER" ] - then - NEW_KUBECONFIG=$($EXECUTABLE_PATH gardener "$GARDENER_ARGUMENT" \ - $KUBECONFIG_PATH_FLAG ${KUBECONFIG_PATH} \ - $DEBUG_FLAG ${DEBUG} \ - $CONFIG_PATH_FLAG ${CONFIG_PATH} - ) - - setKubeconfigEnvironmentVariable $NEW_KUBECONFIG - return - fi - - if [ -n "$NAMESPACE" ] - then - $EXECUTABLE_PATH ns \ - "$NAMESPACE_ARGUMENT" \ - $KUBECONFIG_PATH_FLAG ${KUBECONFIG_PATH} \ - $DEBUG_FLAG ${DEBUG} \ - $NO_INDEX_FLAG ${NO_INDEX} - - return - fi - - if [ -n "$HOOKS" ] - then - echo "Running hooks." - - HOOK_NAME_FLAG='' - if [ -n "$HOOK_NAME" ] - then - HOOK_NAME="$HOOK_NAME" - HOOK_NAME_FLAG=--hook-name - fi - - RUN_IMMEDIATELY_FLAG='' - - # do not set flag --run-immediately for hooks ls command - if [ "$HOOKS_ARGUMENTS" != ls ]; then - if [ -n "$RUN_IMMEDIATELY" ] - then - RUN_IMMEDIATELY_FLAG=--run-immediately - RUN_IMMEDIATELY="$RUN_IMMEDIATELY" - else - RUN_IMMEDIATELY_FLAG=--run-immediately - RUN_IMMEDIATELY="true" - fi - fi - - RESPONSE=$($EXECUTABLE_PATH hooks \ - "$HOOKS_ARGUMENTS" \ - $RUN_IMMEDIATELY_FLAG=${RUN_IMMEDIATELY} \ - $CONFIG_PATH_FLAG ${CONFIG_PATH} \ - $STATE_DIRECTORY_FLAG ${STATE_DIRECTORY} \ - $HOOK_NAME_FLAG ${HOOK_NAME}) - - if [ -n "$RESPONSE" ] - then - echo $RESPONSE - fi - return - fi - - # always run hooks command with --run-immediately=false - $EXECUTABLE_PATH hooks \ - --run-immediately=false \ - $CONFIG_PATH_FLAG ${CONFIG_PATH} \ - $STATE_DIRECTORY_FLAG ${STATE_DIRECTORY} - - # execute golang binary handing over all the flags - NEW_KUBECONFIG=$($EXECUTABLE_PATH \ - $SET_CONTEXT \ - $KUBECONFIG_PATH_FLAG ${KUBECONFIG_PATH} \ - $STORE_FLAG ${STORE} \ - $KUBECONFIG_NAME_FLAG ${KUBECONFIG_NAME} \ - $SHOW_PREVIEW_FLAG=${SHOW_PREVIEW} \ - $VAULT_API_ADDRESS_FLAG ${VAULT_API_ADDRESS} \ - $STATE_DIRECTORY_FLAG ${STATE_DIRECTORY} \ - $DEBUG_FLAG ${DEBUG} \ - $NO_INDEX_FLAG ${NO_INDEX} \ - $CONFIG_PATH_FLAG ${CONFIG_PATH}) - - setKubeconfigEnvironmentVariable $NEW_KUBECONFIG -} + if [ ! -e "$new_config" ] + then + echo "ERROR: \"$new_config\" does not exist" + return 1 + fi -function setKubeconfigEnvironmentVariable() { - if [[ "$?" = "0" ]]; then - # first, cleanup old temporary kubeconfig file - if [ ! -z "$KUBECONFIG" ] - then - switchTmpDirectory="$HOME/.kube/.switch_tmp/config" - if [[ $KUBECONFIG == *"$switchTmpDirectory"* ]]; then - rm -f $KUBECONFIG - fi - fi + # cleanup old temporary kubeconfig file + local switchTmpDirectory="$HOME/.kube/.switch_tmp/config" + if [[ -n "$KUBECONFIG" && "$KUBECONFIG" == *"$switchTmpDirectory"* ]] + then + rm -f "$KUBECONFIG" + fi - export KUBECONFIG=$1 - currentContext=$(kubectl config current-context) - echo "switched to context \"$currentContext\"." + export KUBECONFIG="$new_config" fi + printf "%s\n" "$RESPONSE" } diff --git a/pkg/main.go b/pkg/main.go index e64a7ff29..b4b46e0c6 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -59,10 +59,10 @@ var ( logger = logrus.New() ) -func Switcher(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex, showPreview bool) error { +func Switcher(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex, showPreview bool) (*string, error) { c, err := DoSearch(stores, config, stateDir, noIndex) if err != nil { - return err + return nil, err } go func(channel chan DiscoveredContext) { @@ -108,11 +108,11 @@ func Switcher(stores []store.KubeconfigStore, config *types.Config, stateDir str kubeconfigPath, selectedContext, err := showFuzzySearch(kindToStore, showPreview) if err != nil { - return err + return nil, err } if len(kubeconfigPath) == 0 { - return nil + return nil, nil } // map back kubeconfig path to the store kind @@ -124,12 +124,12 @@ func Switcher(stores []store.KubeconfigStore, config *types.Config, stateDir str // use the store to get the kubeconfig for the selected kubeconfig path kubeconfigData, err := store.GetKubeconfigForPath(kubeconfigPath) if err != nil { - return err + return nil, err } kubeconfig, err := kubeconfigutil.NewKubeconfig(kubeconfigData) if err != nil { - return fmt.Errorf("failed to parse selected kubeconfig. Please check if this file is a valid kubeconfig: %v", err) + return nil, fmt.Errorf("failed to parse selected kubeconfig. Please check if this file is a valid kubeconfig: %v", err) } // save the original selected context for the history @@ -142,17 +142,17 @@ func Switcher(stores []store.KubeconfigStore, config *types.Config, stateDir str } if err := kubeconfig.SetContext(selectedContext, aliasutil.GetContextForAlias(selectedContext, aliasToContext), store.GetContextPrefix(kubeconfigPath)); err != nil { - return err + return nil, err } if err := kubeconfig.SetKubeswitchContext(contextForHistory); err != nil { - return err + return nil, err } // write a temporary kubeconfig file and return the path tempKubeconfigPath, err := kubeconfig.WriteKubeconfigFile() if err != nil { - return fmt.Errorf("failed to write temporary kubeconfig file: %v", err) + return nil, fmt.Errorf("failed to write temporary kubeconfig file: %v", err) } // get namespace for current context @@ -163,11 +163,7 @@ func Switcher(stores []store.KubeconfigStore, config *types.Config, stateDir str logger.Warnf("failed to append context to history file: %v", err) } - // print kubeconfig path to std.out - // captured by calling bash script to set KUBECONFIG environment variable - fmt.Print(tempKubeconfigPath) - - return nil + return &tempKubeconfigPath, nil } // writeIndex tries to write the Index file for the kubeconfig store diff --git a/pkg/subcommands/clean/clean.go b/pkg/subcommands/clean/clean.go index 0843515b4..b8754f282 100644 --- a/pkg/subcommands/clean/clean.go +++ b/pkg/subcommands/clean/clean.go @@ -16,7 +16,6 @@ package clean import ( "fmt" - "io/ioutil" "os" "github.com/danielfoehrkn/kubeswitch/pkg/cache" @@ -27,7 +26,7 @@ import ( func Clean(stores []store.KubeconfigStore) error { // cleanup temporary kubeconfig files tempDir := os.ExpandEnv(kubeconfigutil.TemporaryKubeconfigDir) - files, _ := ioutil.ReadDir(tempDir) + files, _ := os.ReadDir(tempDir) err := os.RemoveAll(tempDir) if err != nil { return err diff --git a/pkg/subcommands/delete-context/delete_context.go b/pkg/subcommands/delete-context/delete_context.go new file mode 100644 index 000000000..a5e7d094c --- /dev/null +++ b/pkg/subcommands/delete-context/delete_context.go @@ -0,0 +1,38 @@ +// Copyright 2021 The Kubeswitch authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package setcontext + +import ( + "fmt" + kubeconfigutil "github.com/danielfoehrkn/kubeswitch/pkg/util/kubectx_copied" + "os" +) + +func DeleteContext(desiredContext string) error { + kcPath := os.Getenv("KUBECONFIG") + kubeconfig, err := kubeconfigutil.NewKubeconfigForPath(kcPath) + if err != nil { + return err + } + if err := kubeconfig.RemoveContext(desiredContext); err != nil { + return err + } + + if _, err := kubeconfig.WriteKubeconfigFile(); err != nil { + return fmt.Errorf("failed to write kubeconfig file: %v", err) + } + + return nil +} diff --git a/pkg/subcommands/history/history.go b/pkg/subcommands/history/history.go index f84aae216..ce760f298 100644 --- a/pkg/subcommands/history/history.go +++ b/pkg/subcommands/history/history.go @@ -29,10 +29,10 @@ import ( var logger = logrus.New() -func SwitchToHistory(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) error { +func SwitchToHistory(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) (*string, error) { history, err := util.ReadHistory() if err != nil { - return err + return nil, err } historyLength := len(history) @@ -83,12 +83,12 @@ func SwitchToHistory(stores []store.KubeconfigStore, config *types.Config, state }) if err != nil { - return err + return nil, err } context, ns, err := util.ParseHistoryEntry(history[idx]) if err != nil { - return fmt.Errorf("failed to set namespace: %v", err) + return nil, fmt.Errorf("failed to set namespace: %v", err) } // TODO: only switch context if the current context is not already set @@ -96,19 +96,19 @@ func SwitchToHistory(stores []store.KubeconfigStore, config *types.Config, state // do not append to history as the old namespace will be added (only add history after changing the namespace) tmpKubeconfigFile, err := setcontext.SetContext(*context, stores, config, stateDir, noIndex, false) if err != nil { - return err + return nil, err } // old history entry that does not include a namespace if ns == nil { - return nil + return tmpKubeconfigFile, nil } if err := setNamespace(*ns, *tmpKubeconfigFile); err != nil { - return err + return tmpKubeconfigFile, err } - return util.AppendToHistory(*context, *ns) + return tmpKubeconfigFile, util.AppendToHistory(*context, *ns) } func setNamespace(ns string, tmpKubeconfigFile string) error { @@ -130,14 +130,14 @@ func setNamespace(ns string, tmpKubeconfigFile string) error { // SetPreviousContext sets the previously used context from the history (position 1) // does not add a history entry -func SetPreviousContext(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) error { +func SetPreviousContext(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) (*string, error) { history, err := util.ReadHistory() if err != nil { - return err + return nil, err } if len(history) == 0 { - return nil + return nil, nil } var position int @@ -149,48 +149,48 @@ func SetPreviousContext(stores []store.KubeconfigStore, config *types.Config, st context, ns, err := util.ParseHistoryEntry(history[position]) if err != nil { - return fmt.Errorf("failed to set previous context: %v", err) + return nil, fmt.Errorf("failed to set previous context: %v", err) } tmpKubeconfigFile, err := setcontext.SetContext(*context, stores, config, stateDir, noIndex, false) if err != nil { - return err + return nil, err } // old history entry that does not include a namespace if ns == nil { - return nil + return tmpKubeconfigFile, nil } - return setNamespace(*ns, *tmpKubeconfigFile) + return tmpKubeconfigFile, setNamespace(*ns, *tmpKubeconfigFile) } // SetLastContext sets the last used context from the history (position 0) // does not add a history entry -func SetLastContext(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) error { +func SetLastContext(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) (*string, error) { history, err := util.ReadHistory() if err != nil { - return err + return nil, err } if len(history) == 0 { - return nil + return nil, nil } context, ns, err := util.ParseHistoryEntry(history[0]) if err != nil { - return fmt.Errorf("failed to set previous context: %v", err) + return nil, fmt.Errorf("failed to set previous context: %v", err) } tmpKubeconfigFile, err := setcontext.SetContext(*context, stores, config, stateDir, noIndex, false) if err != nil { - return err + return nil, err } // old history entry that does not include a namespace if ns == nil { - return nil + return tmpKubeconfigFile, nil } - return setNamespace(*ns, *tmpKubeconfigFile) + return tmpKubeconfigFile, setNamespace(*ns, *tmpKubeconfigFile) } diff --git a/pkg/subcommands/list-contexts/list-contexts.go b/pkg/subcommands/list-contexts/list-contexts.go index 06cca8e0c..1318cc344 100644 --- a/pkg/subcommands/list-contexts/list-contexts.go +++ b/pkg/subcommands/list-contexts/list-contexts.go @@ -16,6 +16,7 @@ package list_contexts import ( "fmt" + "strings" "github.com/danielfoehrkn/kubeswitch/pkg" "github.com/danielfoehrkn/kubeswitch/pkg/store" @@ -25,12 +26,13 @@ import ( var logger = logrus.New() -func ListContexts(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool) error { +func ListContexts(stores []store.KubeconfigStore, config *types.Config, stateDir string, noIndex bool, prefix string) ([]string, error) { c, err := pkg.DoSearch(stores, config, stateDir, noIndex) if err != nil { - return fmt.Errorf("cannot list contexts: %v", err) + return nil, fmt.Errorf("cannot list contexts: %v", err) } + lc := make([]string, 0) for discoveredKubeconfig := range *c { if discoveredKubeconfig.Error != nil { logger.Warnf("cannot list contexts. Error returned from search: %v", discoveredKubeconfig.Error) @@ -42,9 +44,10 @@ func ListContexts(stores []store.KubeconfigStore, config *types.Config, stateDir name = discoveredKubeconfig.Alias } - // write to STDIO - fmt.Println(name) + if prefix == "" || strings.HasPrefix(name, prefix) { + lc = append(lc, name) + } } - return nil + return lc, nil } diff --git a/pkg/subcommands/set-context/set_context.go b/pkg/subcommands/set-context/set_context.go index e03899001..4a0854727 100644 --- a/pkg/subcommands/set-context/set_context.go +++ b/pkg/subcommands/set-context/set_context.go @@ -98,8 +98,6 @@ func SetContext(desiredContext string, stores []store.KubeconfigStore, config *t } } - // print kubeconfig path to std.out -> captured by calling bash script to set KUBECONFIG environment Variable - fmt.Print(tempKubeconfigPath) return &tempKubeconfigPath, nil } } diff --git a/pkg/subcommands/unset-context/unset_context.go b/pkg/subcommands/unset-context/unset_context.go new file mode 100644 index 000000000..4ddcf61d8 --- /dev/null +++ b/pkg/subcommands/unset-context/unset_context.go @@ -0,0 +1,39 @@ +// Copyright 2021 The Kubeswitch authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package setcontext + +import ( + "fmt" + kubeconfigutil "github.com/danielfoehrkn/kubeswitch/pkg/util/kubectx_copied" + "os" +) + +func UnsetCurrentContext() error { + kcPath := os.Getenv("KUBECONFIG") + kubeconfig, err := kubeconfigutil.NewKubeconfigForPath(kcPath) + if err != nil { + return err + } + + if err := kubeconfig.ModifyCurrentContext(""); err != nil { + return err + } + + if _, err := kubeconfig.WriteKubeconfigFile(); err != nil { + return fmt.Errorf("failed to write temporary kubeconfig file: %v", err) + } + + return nil +} diff --git a/pkg/util/kubectx_copied/kubeconfig.go b/pkg/util/kubectx_copied/kubeconfig.go index eb4eb8cb0..045b360a9 100644 --- a/pkg/util/kubectx_copied/kubeconfig.go +++ b/pkg/util/kubectx_copied/kubeconfig.go @@ -16,8 +16,9 @@ package kubeconfigutil import ( "fmt" - "io/ioutil" + "github.com/pkg/errors" "os" + "path/filepath" "strings" "gopkg.in/yaml.v3" @@ -34,11 +35,20 @@ type Kubeconfig struct { rootNode *yaml.Node } +// LoadCurrentKubeconfig loads the current kubeconfig +func LoadCurrentKubeconfig() (*Kubeconfig, error) { + path, err := kubeconfigPath() + if err != nil { + return nil, err + } + return NewKubeconfigForPath(path) +} + // NewKubeconfigForPath creates a kubeconfig representation based on an existing kubeconfig // given by the path argument // This will overwrite the kubeconfig given by path when calling WriteKubeconfigFile() func NewKubeconfigForPath(path string) (*Kubeconfig, error) { - bytes, err := ioutil.ReadFile(path) + bytes, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read kubeconfig file: %v", err) } @@ -153,7 +163,7 @@ func (k *Kubeconfig) WriteKubeconfigFile() (string, error) { } // write temporary kubeconfig file - file, err = ioutil.TempFile(k.path, "config.*.tmp") + file, err = os.CreateTemp(k.path, "config.*.tmp") if err != nil { return "", err } @@ -177,3 +187,22 @@ func (k *Kubeconfig) WriteKubeconfigFile() (string, error) { func (k *Kubeconfig) GetBytes() ([]byte, error) { return yaml.Marshal(k.rootNode) } + +func kubeconfigPath() (string, error) { + // KUBECONFIG env var + if v := os.Getenv("KUBECONFIG"); v != "" { + list := filepath.SplitList(v) + if len(list) > 1 { + // TODO KUBECONFIG=file1:file2 currently not supported + return "", errors.New("multiple files in KUBECONFIG are currently not supported") + } + return v, nil + } + + // default path + home := os.Getenv("HOME") + if home == "" { + return "", errors.New("HOME environment variable not set") + } + return filepath.Join(home, ".kube", "config"), nil +} diff --git a/pkg/util/util.go b/pkg/util/util.go index c1db9f27a..e84503e85 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -22,6 +22,7 @@ import ( "gopkg.in/yaml.v3" "github.com/danielfoehrkn/kubeswitch/types" + kubeconfigutil "github.com/danielfoehrkn/kubeswitch/pkg/util/kubectx_copied" ) // GetContextsNamesFromKubeconfig takes kubeconfig bytes and parses the kubeconfig to extract the context names. @@ -77,3 +78,16 @@ func ExpandEnv(path string) string { path = strings.ReplaceAll(path, "~", "$HOME") return os.ExpandEnv(path) } + +// GetCurrentContext returns "current-context" value of current kubeconfig +func GetCurrentContext() (string, error) { + kc, err := kubeconfigutil.LoadCurrentKubeconfig() + if err != nil { + return "", err + } + currCtx := kc.GetCurrentContext() + if currCtx == "" { + return "", fmt.Errorf("current-context is not set") + } + return currCtx, nil +} diff --git a/scripts/_switch.bash b/scripts/_switch.bash deleted file mode 100644 index 5955a0b40..000000000 --- a/scripts/_switch.bash +++ /dev/null @@ -1,92 +0,0 @@ -_kube_contexts() -{ - local curr_arg; - curr_arg=${COMP_WORDS[COMP_CWORD]} - - # if not the first argument, and the previous one is not a flag (so it is a command) - if [ "$COMP_CWORD" -gt 1 ]; then - - case ${COMP_WORDS[COMP_CWORD - 1]} in - alias*) - arguments="ls - rm - --state-directory - --config-path - --kubeconfig-name - --kubeconfig-path - --no-index - --store - --vault-api-address - --help" - ;; - - clean*) - arguments="" - ;; - - hooks*) - arguments="ls - --config-path - --hook-name - --run-immediately - --state-directory - --help" - ;; - - list-contexts*) - arguments="--config-path - --kubeconfig-name - --kubeconfig-path - --no-index - --state-directory - --store - --vault-api-address - --help" - ;; - - *) - arguments="" - ;; - - esac - - if [[ $arguments != "" ]]; then - COMPREPLY=( $(compgen -W "$arguments") ); - return - fi - fi - - if [[ $curr_arg != --* ]]; then - contexts=$(switch list-contexts) - fi - - COMPREPLY=( $(compgen -W "history - help - clean - hooks - alias - list-contexts - --kubeconfig-path - --no-index - --debug - --store - --kubeconfig-name - --show-preview - --vault-api-address - --executable-path - --state-directory - --config-path - --help - -c - --current - -d - -u - --unset - - - . - -v - version - $contexts " -- $curr_arg ) ); -} - -complete -F _kube_contexts switch \ No newline at end of file diff --git a/scripts/cleanup_handler_zsh.sh b/scripts/cleanup_handler_zsh.sh deleted file mode 100644 index a47c08052..000000000 --- a/scripts/cleanup_handler_zsh.sh +++ /dev/null @@ -1,14 +0,0 @@ -# HOW TO USE: append or source this script from your .zshrc file to clean the temporary kubeconfig file -# set by KUBECONFIG env variable when exiting the shell - -trap kubeswitchCleanupHandler EXIT - -function kubeswitchCleanupHandler { - if [ ! -z "$KUBECONFIG" ] - then - switchTmpDirectory="$HOME/.kube/.switch_tmp/config" - if [[ $KUBECONFIG == *"$switchTmpDirectory"* ]]; then - rm $KUBECONFIG - fi - fi -} \ No newline at end of file